0%

简介

本节来看一下如何创建一个玩家对象并让其在游戏框中运动。

玩家类

从简到难,先通过一个简单的方块来表示一个玩家。

在pygame中,所有的对象都称为元素,玩家是一个元素,游戏中的怪物也是一个元素,而所有的元素都要通过pygame.sprite.Sprite来控制,下面就来创建一个简单的玩家类。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Player(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((50, 50)) # self.image表示元素本身
self.image.fill(GREEN) # 元素本身填充为绿色
self.rect = self.image.get_rect() # 获得该元素对应的方块
self.rect.center = (WIDTH /2, HEIGHT / 2) # 将该元素放置在窗口中间(方块代表这元素)

# 更新角色 - 移动
def update(self):
self.rect.x += 5
if self.rect.left > WIDTH:
self.rect.right = 0

我们定义了Player类,它继承于pygame.sprite.Sprite,在__init__()初始化方法中,调用了pygame.sprite.Sprite中的初始化方法,完成基本的初始化。

随后,调用了pygame.Surface()方法创建了一个50x50的正方形,然后将其填充为绿色。最后就是将Player类这个绿色的方法放在游戏框的正中间。

除了定义__init__()方法,Player类中还定义了update()方法,该方法就是让绿色方块沿x轴平移,当绿色方块的left超出游戏框时,就将right置为0,这样就会达到走马灯的效果。

让玩家运动起来

光定义玩家类是无法让玩家元素平移运动的,回顾一下第一篇pygame文章,游戏本质就是循环,每次循环都要处理相应的逻辑,循环中逻辑的不同,造成了不同的游戏,所以为了让玩家元素可以移动,还需要实现这个循环。

首先,依旧是初始化相关的对象。

1
2
3
4
5
6
7
8
9
10
pygame.init() # pygame初始化
pygame.mixer.init() # mixer在pygame中用于处理一切音频相关的东西
screen = pygame.display.set_mode((WIDTH, HEIGHT)) # 初始化窗口
pygame.display.set_caption('My Game') # 标题名
clock = pygame.time.Clock() # 计时器,主要用于控制界面刷新的频率

# 在一个游戏中,通过一个元素组来表示该游戏中所有的角色
all_sprites = pygame.sprite.Group()
player = Player()
all_sprites.add(player) # 添加到元素组中

与此前不同,因为多了玩家元素,所以需要初始化pygame.sprite.Group(),然后将玩家对象添加到元素组中,pygame要求我们通过元素组的方式来操作不同的元素。

初始化完成后,直接一个无限循环,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 所有游戏都是一个循环, 循环中通常由三个部分组成,事件输入、元素状态更新、绘制
running = True
while running:
# 帧率
clock.tick(FPS)
# 处理事件输入
for event in pygame.event.get():
if event.type == pygame.QUIT: #如果点击 x 按钮,游戏退出,即跳出循环
running = False
all_sprites.update() # 更新角色
# 绘制与渲染(依旧是黑色背景)
screen.fill(BLACK)
all_sprites.draw(screen) #绘制所有元素到界面中
# 翻转绘画板,更新界面,绘制任何内容,都需要这个步骤,不然绘制的内容在背面,玩家是不可见的
pygame.display.flip()

为了方便大家理解,代码中我依旧给出了详细的注释,唯一不同就是多了事件输入的处理逻辑以及角色更新的逻辑。

通过pygame.event.get()可以获得pygame可以接收到的所有事件,在这个循环中,判断了事件类型,如果点击了窗口关闭按钮,就将running设置为False,结束无限循环。

此外,调用了元素组中的update()方法,对所有元素的状态进行更新,而当下元素组中只有玩家对象,所有玩家对象的状态被更新了,简单而言,玩家此时沿x轴移动了5个像素。随后的代码都与第一篇相同,不再赘述。

此时运行整个完整的代码,绿色方块就会在游戏屏幕中移动,实现跑马灯效果了。

更进一步

玩家类是一个绿色方块显得太过简陋,这里将其替换为一张图片,此外让玩家可以上下跳动,修改后,玩家类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Player(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
# pygame加载图像元素时,记得要使用convert()方法将图像转为pygame容易操控的对象,否则整个游戏加载会变慢
self.image = pygame.image.load(os.path.join(img_folder, 'p1_jump.png')).convert()
self.image.set_colorkey(BLACK) # 将图像矩阵中除图像外周围的元素都设置为透明的
self.rect = self.image.get_rect() # 获得该元素对应的方块
self.rect.center = (WIDTH /2, HEIGHT / 2) # 将该元素放置在窗口中间(方块代表这元素)
self.y_speed = 5

# 更新玩家状态
def update(self):
self.rect.x += 5
self.rect.y += self.y_speed
if self.rect.bottom > HEIGHT - 200:
self.y_speed = -5
if self.rect.top < 200:
self.y_speed = 5
if self.rect.left > WIDTH:
self.rect.right = 0

上述代码中,利用pygame.image.load()方法实现图像元素的加载,需要注意,加载完后,一定要使用convert()方法将其转为pygame容易操作的对象(这是个Ticks),接着还需要使用set_colorkey()方法将图像元素外的其他元素都设置为透明,不然显示在pygame游戏框中就会是一个正方形,正方形中有张图片,非常的丑。

此外,update()方法也走了一些逻辑修改,让玩家元素除了在x轴移动外,还实现其在y轴移动,移动的方式是上下跳动的形式,最终效果如下。

本节介绍了pygame怎么样去管理元素,在下一节中,将介绍pygame如何控制元素移动,即通过键盘控制游戏框中元素的移动。

如果文章对你有所帮助,请点「在看」给作者一点鼓励,叩谢豪恩。

简介

Python是否可以开发简单的游戏?明显是可以的。

在Python中可以利用pygame来开发一款游戏,有了pygame,就不需要我们自己去实现很低层的逻辑,如界面的刷新,物体的碰撞检测等等。

这一系列文章是我个人此前学习笔记加以整理而成(学习内容来自:http://kidscancode.org/),所以开发的游戏并不是我个人原创的,本系列文章会开发一款「跳跳兔」,比互联网上随处可见的飞机大战有趣一些,其最终效果如下。

跳跳兔🐰可以左右移动以及上下跳动,如果获得了蓝色火箭卡片,就可以跳跃比较远的距离,如果碰到了飞行敌人,就会死亡,当然,没有跳动平台上也会死亡。

下面我们就分多篇文章来理解,如何利用pygame来开发这一款麻雀虽小五脏俱全的小游戏。

本系列使用MacOS+Python3.6来讲解。

本篇先从pygame基本使用开会讲解。

pygame安装与介绍

Pygame 是跨平台 Python 模块,专为 电子游戏设计。包含图像、声音。创建在 SDL 基础上,允许实时 电子游戏研发而无需被低级语言,如 C 语言或是更低级的汇编语言束缚。

Pygame 应用程序能够在 Android 手机和平板运行,采用 Pygame 对于 Andorid 的子集 (pgs4a)。支持 Android 的声音,振动,键盘和加速。但缺点是没有办法在 iOS 上运行 Pygame 应用程序。

pgs4a 的主要限制是缺乏对于多点触控的支持, 这使得双指缩放,以及旋转无法使用。另一个 Pygame 在 Android 子集的替代方案是 Kivy,它包含了多点触控及 iOS 的支持。

pygame的安装非常简单,直接pip安装则可

1
pip install pygame

PyWeek游戏制作竞赛

PyWeek 是一个用 Python 语言开发游戏的竞赛,早期多利用 Pygame 作游戏引擎,后来也有很多不同的参赛者使用 Pyglet。这项竞赛开始于 2005 年 6 月,所有游戏必须开放源代码和媒体文件,作者持有版权并以 自由软件的协议发布。

​如果使用者使用第三方的资源来发布游戏,必须确定第三方的资源为公开的 public domain 协议发放。因为开放源代码的特殊性,被众多游戏媒体所忽视。

因为开放源码,PyWeek 上有很多优秀的游戏代码以及游戏资源供我们参考与下载。

点开其中的游戏,可以看到提供的资源文件,我们可以将其下载到本地加以学习与使用。

pygame基本使用

开发一款游戏,本质就是利用计算机不停的刷新屏幕(帧率)并同时判断物体方块之间有没有发生碰撞,我们游戏中的各种元素本质都是一个方块,方块的移动、碰撞就可以实现游戏元素的各种效果。

使用一下pygame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pygame
import random

WIDTH = 360
HEIGHT = 400
FPS = 30 # 帧率,一秒刷新多少次

# define colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

pygame.init() # pygame初始化
pygame.mixer.init() # mixer在pygame中用于处理一切音频相关的东西
screen = pygame.display.set_mode((WIDTH, HEIGHT)) # 初始化窗口
pygame.display.set_caption('My Game') # 标题名
clock = pygame.time.Clock() # 计时器,主要用于控制界面刷新的频率

# 在一个游戏中,通过一个元素组来表示该游戏中所有的角色
all_sprites = pygame.sprite.Group()

在代码中,我给出的了详细的注释,都是一些初始化的逻辑。我们初始化了pygame,然后初始化了mixer来处理音频相关的内容,初始化了窗口并设置了标题名,初始化了计时器,计数器主要用于控制界面刷新的频率,刷新界面的频率也称为帧率。

普通人在传统的显示器上 75HZ 一下就会感觉到闪烁,85Hz 以上才可以。理论上人眼有一个 0.1 秒的视觉延迟,所以通常每秒刷新 10 次以上就可以了,但实际情况是每秒刷新 24 次以上时人眼才会分辨不出来。这里我们使用了30作为帧率,在这个帧率上,人眼看见的内容就会是连贯的。

初始化完成后,还需要知道游戏开发的一个背景知识。

所有的游戏其实都是一个循环,循环中通常由三个部分组成,这三个部分分别是:事件输入、元素状态更新、绘制,不同游戏之所以不同,只是对这三个事件的处理有所不同,下面我来实现最简单的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Game loop 所有游戏都是一个循环, 循环中通常由三个部分组成,事件输入、元素状态更新、绘制
running = True
while running:
# 让循环以正确的速度运行 - 即以正确的帧率运行
clock.tick(FPS)

# some code
# 处理事件输入并实现各种相应的逻辑。
# some code

for event in pygame.event.get():
if event.type == pygame.QUIT: #如果点击 x 按钮,游戏退出,即跳出循环
running = False
all_sprites.update() # 更新角色
# 绘制 / 渲染
screen.fill(BLACK)
# 翻转绘画板,更新界面,绘制任何内容,都需要这个步骤,不然绘制的内容在背面,玩家是不可见的
pygame.display.flip()

代码中有详细的注释,唯一需要解释的就是:为何需要使用「pygame.display.flip()」实现绘画的反转,为什么要以这种方式来更新界面?

这是为了避免界面卡顿与界面元素混乱等现象。

可以将屏幕想象成一张卡片,卡片的正面是我们观察到的界面,此时pygame会将下一帧要绘制/渲染的内容生成到卡片的背面,当要展示时,再将卡片翻转,此时正面变为背面,背面变为正面,pygame会将背面内容清除,然后再重新绘制。

运行上述代码,获得如下效果。

本节简单的介绍了Pygame相关的一些内容,此外也介绍了开发游戏的基本思路。

在下一篇文章中,我们将引入玩家类,看一下pygame是如何操作元素的。

如果文章对你有所帮助,请点「在看」给作者一点鼓励,叩谢豪恩。

重要提示,本文只会提供关键名词,不会涉及太多技术细节,技术细节自己去深挖吧。

在 Python 中加快文件传输和文件复制 - Giampaolo Rodola

第一位上台的是来自意大利的Giampaolo Rodola,一位Python核心开发者,本来想上去尬聊的,但限于自己的英语水平,就作罢了,在这场会议上并不是每个单词都懂,但就是大致知道他在分享什么。

他的分享分为两大块。

第一部分讲了Python3.8中拷贝的底层使用了os.sendfile()或socket.sendfile()方法实现文件的拷贝,相比于旧的拷贝方式,这种方式会更快。

操作系统分为用户态与内核态,旧的复制文件方式会多次在用户态与内核态之间切换执行,而os.sendfile()会在内核态完成所有的操作,所以更快。

第二部分主要介绍了psutil,通过psutil可以实现对计算机大部分状态的监控,如cpu、内存、磁盘、进程、网络等各种状态,利用psutil其实就可以构建出一个计算机状态监控器了。

Google SRE 体系核心基础解读 —— 刘征

第二位上台的是刘征老师(Elastic的广告),主要讲了Google的SRE(网站可靠性工程),SRE主要有3个东西,分别是SLA、SLO、SLI,在边听会议的时候,简单查了一下,个人感觉,这种东西更偏向于一种工作方式,感兴趣的可以搜索了解一下

从 Python 开始钱赚钱 —— 邝泽徽

邝泽徽老师主要分享他个人的业余项目,如何利用Python抄虚拟货币赚钱,对于做过一段时间量化的我来说,这个分享没有特别强的逻辑支持,主要使用网格策略,这种策略理论上在大波动的市场中会有比较好的效果,而虚拟货币就是一个大波动的市场,网格策略的核心逻辑就是利用波动做买入与卖出,下跌时买入,上涨是卖出,做到多次下跌买入时的平均价格小于多次上涨卖出的平均价格减去交易手续费的平均价格则可。

这位老师分享的项目比较随意,感觉有一些点是逻辑非自洽的,但这个分享的关键其实并不是项目本身,而是知行合一的理念,知行合一出自王阳明的阳明心学,王阳明受挫后在龙场悟道,提出了知行合一这种心学。(王阳明的书籍值得一看,我被之前棋盘公司技术经理拉坑看完了)。

一行代码加速科学计算 —— 解超

解超老师是位年轻人,声音洪亮的分享了Modin这个库,它可以通过一行代码加速pandas,怎么个一行法?

1
2
3
4
5
import pandas as pd

改为

import modin.pandas as pd

Modin以及实现了70%的Pandas API,使用方式与Pandas完全相同。

Pandas之所以慢,是因为Pandas只能使用CPU的单核,Pandas不是C实现的吗?没错,但人家没有实现支持多核使用的逻辑,Modin的主要改进就是可以利用设备中所有的CPU资源,从而实现速度上的极大提升。

但Modin的社区是否完善?社区不完善遇到坑可是非常非常痛苦的。

感兴趣的可以看一下,Modin 项目仓库地址:

https://github.com/modin-project/modin

数字货币交易系统架构和 Python 实现 —— 黄毅

黄毅老师功力深厚,会议结束后,我特意找他尬聊了一会,因为没有深入研究过Redis,所以很多东西都是知识范围外的。

黄毅老师分享了自己构建的数字货币交易系统的架构,与传统Web系统架构不同的是,交易系统撮合的功能必须在全局顺序执行,所谓撮合就是找到卖方的最低价与买方最高价,让双方进行交易。

这就需要找到整个系统中的买方,然后找到整个系统的卖方,然后按顺序进行交易,这部分是无法实现并发的,即没办法多笔交易同时进行,因为每一笔交易都会影响到下一笔交易。

这让系统存在理论上的承载上限,无法以增加机器构建集群的方式来扩展系统。(我非常好奇A股交易系统、美股交易系统是怎么解决这问题的?)

黄毅老师的解决方式是使用Redis Module实现新的Redis数据类型,来满足业务逻辑,让业务逻辑全部在内存中完成。当然,数据会持久化的记录于关系型数据库中(Ticks:拼接成批量操作的SQL,增加插入数据速度)。

交易系统依托于Redis Module与Redis Stream(Redis 5.0以上才支持,类似与Kafka),可以实现单核每秒十万笔交易的程度(大喊666)。

大佬分享了他们的流计算开源作品:https://github.com/cryptorelay/redis-aggregation

他们公司 Crypto 还有招聘,薪资不是一般的高,Python开发:50k-100k(经验:5-10年)。

Python 的人工智能开发在微软云中的应用 —— 卢建晖

卢建晖老师-微软最有价值专家

可能是老师深藏不漏,我没有Get到演讲的神韵,虽然分享题目是人工智能这块的内容,而且大部分介绍Azure,即广告。

因为我做过一段时间的NLP,大致知道情况,深度学习目前对个人玩家并不友好,目前知名模型训练需要耗费巨大的算力,算力等于钱,需要上集群,这种云上免费训练,只能做一些简单模型。

当然我们可以利用迁移学习来使用他人已经训练好的模型,但这与分享的东西就没啥联系了。

此外,一个比较有意思的就是VS Code支持了Jupyter插件,可以直接在VS Code中使用Jupyter,而且更加智能方便。

VS Code使用Electron开发,本身又开源,其代码很值得学习,推荐一个来自淘宝前端大佬的博客,他此前一段时间的工作就是魔改VS Code,形成淘宝自己的开发工具,名为Editor,其博客如下:

https://www.barretlee.com/blog/2019/08/03/vscode-source-code-reading-notes/

FPGA 助力 Python 加速计算 —— 陈志勇

陈志勇老师主要分享了FPGA这种可以半定制的电路,利用FPGA+编程可以实现一些有趣的效果。

一开始主要介绍FPGA硬件上的知识,硬件上的并行就是利用多个电子元件实现的,而单个电子元件只能实现并发的效果。

此外还提了函数式编程语言,陈志勇老师说在硬件上编写程序一定要有函数式编程思维,因为我只用过Erlang这一种函数式编程语言做游戏开发,所以并不太理解这句话。

这个分享唯一与Python相关的地方就是PYNQ库,利用PYNQ的API可以编写在该公司硬件上使用的程序,运行速度很快,原因在于PYNQ会将相应的Python语言映射为硬件设备上RTL代码,从而实现极快的运算速度。

除了可以使用Python编写外,还可以使用C来编写,利用Vivado HLS这个工具,可以将C语言转为RTL代码,转换的过程应该是利用了编译原理相关的知识,但转换效果没有利用PYNQ这种映射成RTL的方式好,原因在于编写的C语言没有使用硬件开发的思路来写代码,此时转为的RTL代码其实写的不好,导致效率不高,而PYNQ这种方式,以丧失灵活性的方式来实现映射后RTL代码的规范性。

其实很多工具代码转换的方式都会有各种各样的问题,如Unity开发游戏转为H5,H5确实可以运行,但卡的不行,手机上完全不能玩,此外Debug等各种问题也是坑。

Pipenv 和 Python 包管理 —— 明希

明希老师主要分享了Python虚拟环境以及包管理的一些内容,内容很细,一开始主要分享了安装包的正确方法以及各种各样的坑。

随后介绍了Pipenv,简单说了依赖解析问题与相应的解决方法。

最后说了一下为了Python包管理未来可能出现的方式,涉及了PEP517、PEP518草案,说的其实就是Node.js利用npm管理包那套,像npm那样,你可以选择将依赖库安装全局也可以安装到当前项目的目录下,安装在当前项目目录,只会被当前项目使用,利用这种方式就不用理会虚拟环境的问题了。

然后介绍了PyFlow这一个工具,可以实现PEP517、PEP518草案的效果,地址为:

https://github.com/David-OConnor/pyflow

但npm本身也有各种问题,在2019的JSconfEU上提出了Tink下一代包管理器技术,意在取代npm。

闪电演讲

每个演讲大概10分钟

Python C 拓展在各平台的打包与发布 —— 赵丰

赵丰老师介绍了在CI(Continuous Integration,持续集成)环境自动打包Python C拓展库的方法。

构建Python C拓展包与构建纯Python包不同,需要涉及到编译的流程。在Linux中,随便编译的C拓展库是无法上传到官方的,这里Python官方给出了一个centos6.1环境的Docker,必须在这个Docker的Linux下打的包才能上传。

为 Python Function 自动生成 Web UI —— 彭未康

彭未康老师介绍了自己开发的工具Touch-Callable,构建于Flask之上,可以快速的通过一个方法构建出一个web界面,方便测试人员使用,比较简单,效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# examply.py
from enum import Enum


class 开关(Enum):
开 = '开'
关 = '关'


def 饮水机(口令: str, 制热: 开关=None, 制冷: 开关=开关('开')):
"""这是 20618 的!"""
if 口令 != '多喝热水':
raise ValueError('你是谁,我不认识你')

# 省略具体逻辑

github:https://github.com/pengwk/touch-callable

数字货币交易系统 Python 实践 —— 代少飞

介绍了开发交易系统时会遇到的问题以及给出的解决方法,这些解决方法出乎意料的朴素简单,并没有涉及什么高深技术。

此外还介绍了APScheduler这一个定时任务库(因为他们的系统中使用了),这个库有比较多的概念,有兴趣可以看一下:

https://github.com/agronholm/apscheduler

但如果只是单纯的定时任务,其实并不建议使用apscheduler,它会增大系统的复杂度,直接使用crontab简单粗暴。

Django 实现后端低代码开发平台 —— Jeff

low-code(低代码),简单而言就是通过不写或少写代码的方式来构建一个系统,jeff老师将其分为3个阶段,第一个就是像 Django Admin那样,写少量代码,实现web功能,第二个阶段就是通过配置文件来构建web系统,例如通过JSON文件构建一个web系统,第三个阶段,就是通过界面配置来构建一个web系统。

但我个人觉得,第三阶段应该是以可拖动式的方式来构建一个web系统。因为通过界面配置其实本质依旧是生成一个JSON文件,如果逻辑要变动,还是需要手写逻辑,不够灵活,而目前我知道的商用low-code平台是利用类似逻辑图的结构来构建无代码构建web平台的目的(很多量化平台其实也有类似的东西,如bigquant)。

通过界面拖动的方式,会更加灵活,可以自己构建各种逻辑,但操作复杂度也变大,很多时候构建一个web,如构建后台,通过配置来构建更佳,因为大多数时候都是增删改查,没有什么特别的逻辑。

这其实是我第一次参数这种会议,因为此前自己粗浅的认为,会议没有什么意义,一天能学什么?所以都没有怎么参加,但这次参加感觉很不错,认识了几位新朋友,开阔了一下眼界,这就是会议的意义。

最后,感谢你的阅读,如果内容对你有点帮助,麻烦点一下「好看」,那是可以点击的,谢谢。

如果我来做个「ZAO」换脸app,全网最硬核换脸技术简析(万字长文)

简介

最近,一款名为「ZAO」的AI换脸应用火爆了起来,在各大网站和朋友圈都看见它的身影,它可以通过用户上传的一张带有人脸的照片替换到视频的人脸中,效果非常逼真,引起轰动。

因为「ZAO」团队并没有公开该软件使用的技术,所以我无法确切的判断「ZAO」使用了什么具体的技术,而本篇文章的重点不是剖析「ZAO」应用的技术,而是介绍如何通过已知的技术实现一个自己的「ZAO」,简而言之,就是分析当前的换脸技术。

本篇文章会尝试使用最简单的语句让大家理解其中的关键概念,会从最基本的神经网络开始介绍,让没有任何概念的朋友可以轻易食用。

大致内容:

  • (1)什么是神经网络?
  • (2)怎么训练神经网络?
  • (3)使用卷积神经网络识别图像
  • (4)MTCNN人脸检测技术
  • (5)VAE与GAN简介
    • 变分自动编码器VAE简介
    • 生成对抗网络GAN简介
  • (6)Pix2Pix替换人脸
  • (7)CycleGAN替换人脸
  • (8)Faceswap-GAN换脸应用
  • (9)一张图像实现视频换脸
  • (10)这种技术带来的威胁
  • (11)AI对AI,识破假视频
    • 使用循环神经网络识别视频
    • 通过眨眼生理信号识别视频
    • 通过肖像中的生物信息识别视频
  • (12)结尾
  • (13)参考

别慌,你是可以懂的。

文章前半部分的内容都是用于铺垫,从而让你有背景知识可以明白换脸究竟是怎么回事。

注意文章标题,使用「简析」,即只能简单的分析,因为细节之多,一文难以全部叙述完,这里尽量不涉及太多细节与公式推导,但这也会带来一定的「知识失真」。

文章的图像、训练数据、代码、Paper等,都会在最后一节参考中给出。

1.什么是神经网络?

神经网络(Neural Network,NN)是一种数据模型,更具体而言,就是一个函数。

在20世纪,心理学家McCulloch和数学家Pitts受生物神经元结构的启发提出了MP模型。

MP模型抽象简化了生物神经元结构的细节,它的出现为神经网络打下了一个基础。

生物神经元会接收到其他神经元的电信号输入,在进行简单的处理后,会将处理后的信息传递给其他生物神经元,而MP模型也一样,它会接受到其他模型的信号x1,...,xix_1,...,x_i,然后与权重相乘,并通过某个函数运算后获得新的信号OjO_j,最后将其传递给下一个神经元,公式如下:

oj=f(i=0n(wijxi))o_j = f(\sum_{i=0}^n(w_{ij}x_i))

MP模型可以算是最初的开始,但与现在的神经网络有非常大的差异,现在的神经网络中通常会涉及神经元、层与权重的概念,一个简单神经网络模型如下:

图中的每个圆,可以看做是一个「神经元」,每个神经元本身可以看做是一个函数,神经元被多个带箭头的线连接,这些线表示着数据流向,即函数的输入数据与输出数据的流向。

从图中也可以看出,多个神经元会组成列,每一列中的神经元是没有被带箭头的线相连接的,这其实就构成了「层」。

通常,不同的「层」会因功能不同而叫法不同,如神经网络的第一层,通常称为「输入层」,因为第一层要负责接收外面数据的输入,而神经网络中的最后一层,通常称为「输出层」,因为会将整个神经网络处理后的数据输出,而「输入层」与「输出层」之间的层,就称为「隐藏层」。

权重,就是带箭头线上的值,某个神经元输出的内容会与对应的权重做运算,运算的结果会作为下一层中某个神经元的输入。

2.怎么训练神经网络?

简单明白了神经网络的结构后,接着要思考的问题是,人们常说的训练神经网络是什么?要怎么才能训练神经网络?

训练神经网络分为大体可以分为2大阶段,第一个阶段,称为「前向传播」,第二阶段称为「反向传播」。

在前向传播阶段中,数据会从输入层一直向下一层传递,直到传递到输出层,然后输出一个结果,前向传播的本质就是矩阵运算,依旧是该图。

从图中可以看出,该神经网络具有一个输入层,由3个神经元组成,有一个隐藏层,由4个神经元组成,最后就是输出层,由2个神经元组成。

首先输入层会接收到要输入神经网络的数据,输入层会对其进行预处理,使输入数据成为3维的列向量,因为输入层只有3个神经元。

O0=f(x)O_0 = f(x)

f(x)f(x)为输入层的数据预处理函数,O0O_0为输入层预处理后得到的3维列向量。

随后,O0O_0会与权重矩阵相乘,其结果传递给相应的激活函数,获得结果。

O1=f(W1O0)O_1 = f(W_1 * O_0)

这里的ff函数表示的是隐藏层的激活函数,O0O_0是输入层的输出数据,这里作为隐藏层的输入数据传入,W1W_1是隐藏层的权重矩阵,公式运算的结果O1O_1就是隐藏层的输出。从上面的神经网络结构图可以看出权重矩阵W1W_1是一个4*3的矩阵。

最后,O1O_1会作为输出层的输入,经过类似的运算获得该神经网络的最终输出结果O2O_2

O2=f(W2O1+b2)O_2 = f(W_2 * O_1 + b_2)

W2W_2为输出层的权重矩阵,从上图可以看出它是一个2*4的矩阵。

可以总结出前向传播算法普遍公式:

Oi=f(WiOi1+bi)O_i = f(W_i * O_{i-1} + b_i)

当前向传播阶段的矩阵计算完成后,神经网络会输出一个结果,这个结果并不一定是正确的结果,这是当前神经网络输出的结果,比如,输入的数据是一张人手写的数字1时,我们希望神经网络可以输出数字1作为结果,但神经网络并不一定会按我们的期望输出1,它更大的概率是输出其他的内容。

为了让神经网络输出正确结果,就要量化的表示出当前的输出结果与正确结果之间的差距,这种差距通常称为损失,比如,正确结果是数字1,而神经网络输出的数字9,此时数字1与数字9之间就有一个损失,定义损失的方式有很多种,会涉及不同的损失函数,比较常见的有均方差损失(MSE)、交叉熵损失等。

有了损失后,就需要进行反向传播,所谓反向传播其实就是损失反向传递到神经网络的神经元中,调整带线箭头中的权重,这样的调整最终会影响到神经网络的输出。

在反向传播的过程中,有分为2大步骤,第一步是利用反向传播算法去计算每个神经元对最终损失的贡献度,这个贡献度通常被称为「梯度」,第二步就是通过梯度下降算法将「梯度」运用到不同神经元上,从而实现对其权重的修改。

这里需要强调一下,反向传播算法仅指计算梯度的方法,而随机梯度下降才是使用梯度进行学习的,这点很多博客与书籍都混淆了。如有疑问,请阅读Lan Goodfellow等人著的经典书籍《Deep Learning》的「6.5 反向传播和其他的微分算法」章节内容。

因为反向传播过程涉及较多微积分(偏导数、方向导数等)概念,本文不再深究。

简单总结,神经网络的输入层会接收到输入的数据,然后通过「前向传播」的过程获得一个输出值,将输出值与标准答案进行「损失」的计算,接着将计算出的损失通过「反向传播」的过程作用到神经网络的神经元上,改变神经网络中的结构,我们可以将神经网络整体看成一个函数f,改变其中神经元的权重,相当于改变了函数f的参数,一个函数的参数被改变了,其输出的结果当然也会跟着改变,而这种改变是有方向性的,每次的改变是为了让神经网络输出的值更接近与正确值,通过成千上万次的训练,每次都会通过相同的方式去修正神经网络的参数,最终获得一个可以输出正确值的神经网络模型,这个过程就是完整的训练过程。

需注意,文中的「反向传播」指的是一个过程,包含使用「反向传播算法」与「梯度下降算法」的过程,而不是指「反向传播算法」。

注意,本节谈及的「训练」只指有监督学习中的训练。

3.使用卷积神经网络识别图像

卷积神经网络(Convolutional Neural Network, CNN)是一种擅长处理图形数据的神经网络结构,深度学习中很多图像识别、图像处理相关的应用都有CNN的影子。

对于一张图像,我们人类可以很快的识别出图像中的东西,但对于于计算机来说,它们看到的只是一堆数字,根本不能直观的理解这些数学背后表示的图像。要让计算机可以识别图像,第一步要做的就是让计算机可以理解代表图像的这些数字,如下图,我们可以很快的看出图中有三只短腿小狗,而计算机却不能。

解决这个问题的灵感也来自于生物本身,生物是怎么「理解」看见世界的?对生物而言,它们看到的只是光线照射到某个物体上带来的像素信息,这些信息并没有告诉我们图中有3只小狗。

其中的关键在于,生物可以很轻松的通过很底层的基础信息获得这些信息背后的抽象认知,如人类看见小狗的图像其实就是对大量像素信息这一类底层信息抽象得到图中小狗的。

而卷积神经网络(CNN)原理其实也是这样,将信息抽象成更高的信息,然后更高的信息再进一步抽象。

简单而言就是通过一种叫做过滤器的矩阵(本质就是一个二维数组)与图像中的数据进行运算,获得抽象层,抽象层中的信息就是更高一层的信息,然后以同样的方式再通过过滤器与当前抽象层进行运算,获得下一个更高信息维度的抽象层。

这样,一层层的将信息抽取出来,最终获得可以判断当前物体是什么的信息。

例如,要「看见」图像中的建筑,一开始输入的建筑图像对计算机而言只是一堆看似无用的数字,然后通过一层层的抽象,如第一层抽象,从无用的数字中过滤出了线条,然后再抽象,从线条这个抽象层中抽象出了矩形,然后再抽象,获得长方体,最终获得建筑的轮廓。

具体怎么做到的?

比如要判断图中是否存在老鼠,首先定义出一个过滤器矩阵,它可以从原始图像数据中判断出曲线。

接着让过滤器扫描老鼠图像。

如果曲线过滤器在图像中遇到了曲线,则进行矩阵点积运算时,会获得一个比较大的值,作为下一层中某个神经元的输入。

如果曲线过滤器遇到其他形状,此时矩阵点积运算时,会获得一个较小的值。

通过曲线过滤器完整扫描完老鼠图像后,就获得曲线的抽象层了,上面只演示了一种过滤器的情况,一般会有多个基本的过滤器去扫描图像,从而获得不同的特征(卷积层的深度就是过滤器的个数x过滤器的深度)。

这里只是提及了卷积神经网络的大致原理,要深入理解,还需要理解卷积层、池化层/采样层、步数、填充等概念。

4.MTCNN人脸检测技术

要实现换脸,通常第一步就要检测出图像中人脸的位置,而视频中人脸的检测与在图像中检测的原理是相同的,只是视频需要逐帧去检测。

人脸检测的方式有多种,这里主要介绍MTCNN,主要是因为Faceswap-GAN这款开源的换脸应用使用了MTCNN,其基础就是CNN识别图像中的数据。

MTCNN(Multi-task Cascaded Convolutional Networks)是2016年提出的人脸检测模型,它由3个CNN构成,3个不同的CNN负责不同的功能,实现对图像中的人脸进行检测和特征点的识别。

这3个CNN在MTCNN的论文中分别被称为P-Net、R-Net与O-Net。

上图表明了MTCNN的大致流程:

(1)构成图像金字塔(Image Pyramid):重塑输入的图像,获得不同尺寸的图像,将不同尺寸的图像从大到小的堆叠在一起,类似于金子塔形状,这一步相当于数据的预处理,将原始的图像数据处理成图像金字塔,再使用该数据进行训练。

(2)第一步:使用提案网络(Proposal NetWork, P-Net)获取图像中所有可能含有人脸的部分,即绘制出候选边界框(Proposal Bounding boxes,直译为提案边界框,为了方便理解,这里使用候选边界框,两者含义相同),这些边界框由相应的算法完整扫描完图像后产生,通常会产生非常多的边界框,这是为了避免图像中人脸很小或者人脸没有完全显示等各种情况以及这样可以增强神经网络的鲁棒性,接着使用了NMS(非极大值抑制算法)或Bounding-box regression(边框回归)去除多余的框,从而得到初步的人脸检测候选边界框。这一步是MTCNN中最耗时的,也是MTCNN慢的原因。

(3)第二步:将P-Net获得的人脸图像输入到精细网络(Refinement NetWork, R-Net)中,R-Net会进一步去除多余的框,从而得到更加精细准确而且冗余更少的候选框。

(4)第三步:将R-Net获得的人脸图像输入到输出网络(Output Network, O-Net)中,O-Net进一步对人脸候选框进行细化,并且绘制出人脸中的5个关键点(左眼、右眼、鼻子、左嘴角、右嘴角)对应的坐标。

MTCNN训练时,第一步会消耗大约整个训练过程中3/4的时间,是非常耗时的,其原因在于:

  • 1.要生产图像金字塔,这需要扫描完整的图像,然后逐个运算生产;
  • 2.生产图像金子塔后,每种不同尺寸的图像都要输入模型进行训练,这相当于一张原始图像要进行多次模型的推断;

已经有一些方法被提出,尝试改善训练的耗时。

上图总结了几种多尺度对象提案网络(Multi-scale Object Proposal Network)的方式,MTCNN第一步使用的就是其中的(a)。

目标检测的本质其实就是图像目标区域内容的特征与学习模板权重这两个矩阵之间的点积运算,如果学习模板的尺寸与目标区域的尺寸匹配,就会有比较高的识别率。

而上图中的(a),构成图像金字塔,目的是通过图像的多次缩放,实现训练单个分类器可以匹配所有不同尺寸大小的图像,这种策略需要在多个图像尺寸间进行特征计算,运算量大,导致运行慢。

所以就有另一种方法,即使用多个分类器应用于单个输入的图像,如上图中的(b),这种方式避免了重复的特征计算,但检测效果并不好。

随后就有综合(a)、(b)两种方法的©,即减少图像缩放的次数以及增加分类器的个数。

更进一步,如上图中的(d),先进行少量的缩放,然后自行插入缺失的特征映射,这种方式相当大程度的加快了运行速度并且也可以获得适度的精确度。

上图中还有多种方法,感兴趣可以阅读参考小结中的「论文2」。

MTCNN的训练方式与第二节中介绍的训练方式一样,MTCNN人脸检测网络主要使用了WIDERFace开源人脸检查数据,该数据提供了不同类别的人脸图像数据,这些图像中的人脸都被标注出了正确位置,这并不是指,图像中存在绿色人脸标记框,而是每张图像有对应标签,标签中包含了当前图像中,人脸标记框的左上角坐标以及标记框的宽与高,通过标签中的这些数据可以绘制出标记框。

MTCNN训练时,会获取WIDERFace中的人脸图像数据,然后尝试给出图像中人脸的标记框,接着计算这个标记框的位置与当前输入图像对应标签中真实标记框的位置的损失,通过损失来完善MTCNN模型,直到MTCNN可以标记出人脸的位置。

MTCNN除了可以标记人脸,还可以获得人脸中的5个关键点,它使用了CNN_FacePoint数据集中的人脸数据。

训练原理是相同的。

3个CNN的大致结构如下(有相关经验的人可以明白其大致网络结构,不明白的跳过则可)

5.VAE与GAN的简介

了解了人脸检测后,接着就是人脸生成了,更广义的说,其实就是图像生成,而变分自编码器(Variational Auto-Encoder,VAE)与生成对抗网络(Generative Adversarial Network,GAN)是这一领域的好手。

变分自编码器VAE简介

先从VAE开始,要理解VAE,有必要理解AE(Auto-Encoder,自编码器),所谓Auto-Encoder其实很好理解,它的本质依旧是一个神经网络,只是这个神经网络有编码器(Encoder)、Bottleneck(瓶口)与解码器(Decoder)构成。

输入真实的图像数据给编码器Encoder进行编码操作,所谓编码操作可以理解成抽取图像数据中的特征信息,相当于做了一个压缩的过程,这些特征信息的数据量会明显少于原始图像的数据量,抽取出的特征数据会放在Bottleneck中。

Bootleneck并不会做什么处理,只是用于存储特征数据的网络结构,它会将数据直接传递给解码器Decoder,解码器就会尝试利用这些特征信息还原会图像数据,即从少量关键数据中还原出原始的图像(这个过程也被称为重构)。

自编码器的训练过程也很好理解,一开始整个自编码器网络还原出的数据会与原本传入的真实数据存在较大的差距,这就是损失,通过「反向传播过程」去优化整个网络结构,让损失最小则可(但通常难以获得全局最优值,只是获得局部最优)。

这种神经网络能干什么?

将一个图像编码后又解码,似乎没什么作用?

非也,Google就尝试使用这种简单的技术来提升自己的服务质量。

比如,现在要看一张高清大图,服务器直接将大图数据传递过去会耗费大量的带宽,用户也需要较长时间去等待图像的加载。

此时就可以训练好一个自编码器,将自编码器的结构简单拆分,服务器上用编码器对原始图像进行编码获得特征信息,服务器只需要将少量的特征信息传递给用户的客户端,而客户端就可以使用解码器,通过少量的特征数据运算还原出高清大图了,虽然此图非彼图,通过这种方式,就可以减少宽带的使用。

其实稍微调整一下思路,就可以获得一个可以去除图像杂质或马赛克的网络,如下图:

上图中,并没有直接向编码器中传入原始的正常图像数据,而是传入添加了噪音的数据,然后再通过自编码器还原数据,而还原数据直接与原始的正常图像数据做损失运算,这样训练出来的自编码器就具有去除噪音的能力了,而去除马赛克的思路是完成相同的。

但现实是残酷的,这种方式虽然简单,但模型的泛化能力并不好,还原后的图像还是有较大的瑕疵。

自编码器无法「创造」逼真的图像数据,我们训练的时候,都是给出一张图像,然后它会还原出一张图像,但是它无法「创造」,所以出现了变分自编码器。

变分自编码器与自编码器不同之处仅在于Bottleneck向量处,它相比自编码多了均值向量(mean vector)与标准差向量(standard deviation vector)。

VAE经过一定的训练后,就可以向均值向量与标准差向量定义出的样本空间进行采样,将采样获得的数据传入解码器,此时解码器就会通过解码还原数据,此时还原出的数据是真实世界中不存在的,这是因为我们传入给解码器的特性信息是从采样空间随机采样的,并不是某张真实图像的特征信息。

通常,在训练VAE时会约束均值向量与标准差向量构成的样本空间分布,使其服从正态分布,即均值向量为0,标准差向量为1。这一点从它的损失函数也可以看出(神经网络输出的值与真实值的损失通过某个函数来定义,这个函数被称为损失函数)。

VAE损失函数如下:

L(θ,ϕ;x)=Eqϕ[logpθ(xz)]DKL(qϕ(zx)pθ(z))\mathcal{L}(\theta,\phi;x) = E_{q_{\phi}}[log p_{\theta}(x|z)] - D_{KL}(q_{\phi}(z|x) || p_{\theta}(z))

如果有信息论基础的朋友可以看出,VAE的损失函数由一个期望值与KL散度这两部分构成,其中KL散度的目的就是约束样本空间,使其服从正态分布。

对于VAE的很多细节,这里展示不去讨论。

有了上面的概念,就可以来讨论如何使用自编码器(AE)来替换人脸了。

从图中可以看出,我们需要通过两堆不同的数据训练两个AE,一个AE用于自动编码女孩照片数据,第二个AE用于自动编码尼古拉斯·凯奇(那个男孩)的照片数据,但需要注意的是,两个AE的编码器共享的部分参数,这样做会让编码器找出两堆不同数据的共同特征。

完成训练流程后,使用编码器对女孩的数据进行编码,获得对应的编码器特征,因为训练时,共享了部分参数,所以这些特征中包含了一些共同的特征,此时再用解码器去解码这些特征,就会获得一个换脸后的人了。

这样之所以可行是因为人脸有很多潜在的相同特征,如眼睛的数量位置、鼻子的数量位置等等,通过共享参数的方式,让两个AE的编码器中的部分参数共享,让其可以找到人脸图像中共同的特征数据,此时使用不同的解码器就实现了人脸的替换。

但AE或VAE有一个致命的缺陷就是生成的图像会比较模糊,下面来讨论一下生成对抗网络GAN。

生成对抗网络GAN简介

生成对抗网络(Generative Adversarial NetWork,GAN)的核心思想很简单,传统的GAN其神经网络主要有生成网络(Generator Network)与判别网络(Discriminator Network)构成,两者相互对抗、博弈,最终让生成器(生成网络的别名)可以生成逼真的图像。

举个具体的例子,明白其原理。

小吕是艺术学院的学生,廖老师是学校的老师。

小吕虽然考入的艺术学院,但绘画能力还比较差劲,而廖老师看过很多优秀的画作,知道优秀的画作应该具有什么特点。

小吕每天画一副画交个廖老师看,廖老师会更具自己的经验给出其改进意见,小吕会吸取这些经验,在明天将画画的更好,就这样,小吕一天天的进步,直到一天画出的画与廖老师印象中的名画没有明显的差异时,小吕就算出师了。

GAN也就是这样,其中生成器就是小吕,而判别器就是廖老师,一开始,生成器获取一堆噪音数据(即无用的随机生成的数据)去生成一张图像,生成的图像会交给判别器判别真假,即判别器会根据自己的经验判断传入的这张图像是真实存在的图像还是生成的图像。

一开始,判别器自己并没有「真实图像」的概念,它同样是通过训练来获得这样的概念的,具体而言就是将真实图像作为判别器的输入,让判别器输出1,通过一轮训练后,判别器此时就有了「真实图像」的简单概念了。

生成器的目标其实就是让自己生成的图像与真实图像相似,从而让判别器无法判别出自己生成的图像是真实图像还是生成图像。

GAN大致训练流程如下,以训练GAN生成图片为例

第一步:初始化生成器和判别器,模型结构中的参数随机生成则可
第二步:在每一轮训练中,执行如下步骤:

  • 1.固定生成器的参数,训练判别器的参数,让判别器有「真实图像」的概念,具体而言
    • 1.1 因为生成器的参数被固定了,此时生成器的参数没有收敛,生成器通过未收敛参数生成的图片就不会特别真实
    • 1.2 从准备好的图片数据库中选择一组真实图片数据
    • 1.3 通过上面两步操作,此时就有了两组数据,一组是生成器生成的图片数据,另一组是真实图片数据,通过这两组数据训练判别器,让其对真实图片赋予高分,给生成图片赋予低分
  • 2.固定判别器,训练生成器,让生成器在判别器的指导下优化自己,具体而言
    • 2.1 随机生成一组噪声喂养给生成器,让生成器生成一张图片
    • 2.2 将生成的图片传入判别器中,判别器会给该图片一个分数,比如0.22,生成器的目标就是使这个分数更高,生成出判别器可以赋予高分的图片

GAN简化后的训练过程如下图

图中有3种线,分别是:

  • 黑线虚线:真实数据的分布。
  • 蓝色虚线:判别器的判别分数
  • 绿线:生成器生成的数据分布

从图中可以看出,一开始(图a),代表真实数据分布的黑虚线与代表生成数据分布的线差异较大,此时代表判别器分数的蓝虚线可以比较准确的判断出真实数据和生成数据,它给真实数据赋予了较高的分值,而给生成数据赋予较低的分值。

随着GAN训练次数的增加,生成器为了生成出可以让判别器赋予高分的数据,生成器生成数据的分布渐渐向真实数据的分布靠拢(图b-c),当生成器完全学习到真实数据的分布情况时,判别器就无法分辨他们的了,也就是无论是真实数据还是生成数据都赋予相同的分数(图d)。

上图中,真实数据的分布是从判别器学习而来的,所以在训练GAN时要先训练判别器,让其获得真实数据的分布作为一个“标准”。

从数学角度来解释:

  • 1.从数据库中拿出真实数据x,将其放到判别器中D(x),目标是让其D(x)输出的值接近1。
  • 2.输入随机噪音z给生成器G(z),生成器希望判别器给自己生成的数据输出的值接近1,D(G(z)输出接近1,而判别器希望自己给生出数据输出的值接近0,D(G(z))输出接近0。

通过公式表达,就可以获得GAN的公式:

minGmaxDV(D,G)=ExPdata[logD(x)]+EzPz(z)[log(1D(G(z)))]min_G max_D V(D,G) = E_{x \sim P_{data}}[log D(x)] + E_{z \sim P_z(z)}[log (1-D(G(z)))]

上述公式中,将D定义为判别器,G定义为生成器。

将上面公式拆分来看:

先看前半段,ExPdata[logD(x)]E_{x \sim P_{data}}[log D(x)]其中ExPdataE_{x \sim P_{data}}表示期望x从PdataP_{data}分布中获取,x表示真实数据,PdataP_{data}表示真实数据的分布,这段公式的意思是:判别器要判别出真实数据的概率,判别器的目标就是要最大化这一项。

接着看后半段,EzPz(z)[log(1D(G(z)))]E_{z \sim P_z(z)}[log (1-D(G(z)))]其中EzPz(z)E_{z \sim P_z(z)}表示期望z从pz(z)p_z(z)分布中获取,z表示生成数据,pz(z)p_z(z)表示生成数据的分布,对判别器D而言,如果向其输入的是生成数据,即D(G(z))D(G(z)),判别器的目标就是最小化D(G(z))D(G(z)),即判别器希望最大化log(1D(G(z)))log(1-D(G(z)))

但对生成器而言,它去希望最小化log(1D(G(z)))log(1-D(G(z))),这就与判别器的目标相冲突的,这也是这种神经网络被称为生成对抗网络的原因。

传统的GAN有较多的缺陷,如生成器与判别器能力失衡造成训练不稳定,模型整体难以收敛(简单而言,就是训练过程不稳定),此外还容易产生模式崩溃或梯度消失的问题,但近年经过各方的努力,GAN展示出了巨大的力量。下图展示了这几年,GAN在人脸生成的上的进步(算力需求也大幅提高,个人玩家几乎玩不起)。

除了在图像生成上,利用GAN还可以做很多有趣的事情。

比如智能PS。

比如通过一张图片生成一段视频。

6.Pix2Pix替换人脸

有了GAN的基本概念后,Pix2Pix就不难理解了。

与传统GAN不同,Pix2Pix中的判别器要判断输入的两张图像是否是真实的一对图像,而生成器也不是从噪音数据中生成图像,而是从某一张图像生成另一张图像,如下图:

判别器的目的除了判断生成的图像是否真实外,还需要判断生成的图像是否与另一张图像可以组成正常的一对图像。

Pix2Pix除了使用标准GAN损失函数外,还使用生成图像与对应真实图像之间的L1距离作为损失,从其论文描述中可知,Pix2Pix利用GAN损失捕捉图像中的高频特征,而利用L1损失捕捉图像中的低频特征。

此外,为了让生成器更加容易生成与输入图像相关的图像,采用了U型网络结构(Unet)。

U型网络中使用了Skip-Connection,简单来说就是将前面层中的一些数据不经过后面层的运算处理(运算会丢失细节),而直接交由较后面的层直接使用。这很大程度让生成器网络结构中的后面几层也得到了很多细节数据,从而让生成器更容易生成与输入图像相关的数据。

训练好Pix2Pix后,就可以实现图像的双域转换了,所谓域指的就是某种类型的图像。

Pix2Pix的整体思想比较简单,但有一个缺陷,就是训练数据不好找,比如我想利用Pix2Pix黑夜转白天的效果,就需要准备一堆黑夜的数据以及对应的一堆白天的数据,一对图像,你就需要在同一个地方,白天拍摄一张,晚上拍摄一张,有很多对这样的图像,才能训练出具有比较好效果的Pix2Pix,但这明显不现实。

但有些需求数据是很好找的,比如去除马赛克,只需要找到一张图像,然后为其打上马赛克就可以构成一对数据了,Pix2Pix可以实现效果不错的马赛克去除工具,比如下面对某些植物进行马赛克的去除,取得不俗的效果。

对真人呢?

而替换人脸其实也是类似的思路,下图就是Brannon Dorsey 使用Pix2Pix实现Person-to-Person的效果,虽然看上去不咋样。

原始的Pix2Pix难以产生高清的图像,所以Pix2PixHD被提出,它在保持了原始Pix2Pix能力的前提下提高了其生成高质量的图像的能力。

7.CycleGAN替换人脸

因为训练Pix2Pix需要成对的图像,而很多时候,成对的数据是难以获得的,而CycleGAN可以解决这个问题,实现两个域内的图像相互转换的目的。

在训练CycleGAN时,并不需要使用成对的数据,这是怎么做到的?

一个直观的想法就是先通过生成器获取域X中的图像,将其转换为域Y中的图像,然后再将其转换回来,形象如下图:

其中域X为马的图像,域Y为斑马的图像。

一开始,通过生成器,将马转换成斑马,即G(X->Y),接着再通过另外一个生成器,将斑马转换为马,即G(Y->X)。

简单而言,生成器接收马的图像生成斑马,然后另外一个生成器接受斑马的图像生成马,此时可以计算原始的马图像与还原生成马图像的损失,论文中将这种损失称为循环一致性损失。

单单这样做还不行,因为在训练过程中,神经网络很有可能发现,你就是想将图像还原回输入图像的样子,那么它会慢慢倾向于不做什么有价值的操作,直接将输入图像的大部分数据直接还原,这并不是我们想要的,所以还需要另外一个损失来判断中间状态的生成的斑马是否真实。

再多加一个相似的结构,就可以构成CycleGAN了

如果觉得上图有些难理解,可以看到下图:

CycleGAN为了让模型训练的更加稳定,相比此前的GAN模型CycleGAN做了如下改变:

  • 1.Instance normalization代替Batch normalization
  • 2.目标损失函数使用了LSGAN平方差损失代替传统的GAN损失
  • 3.生成器中使用了残差网络,可以更好的保存图像的语义
  • 4.使用缓存历史图像来训练生成器,减小训练时的震荡,让模型更加稳定

下图就是我通过CycleGAN训练出的效果。

通过CycleGAN对人进行换脸本质依旧是不同域图像之间的转换。

此外,通过这种技术,还可以做一些变态的事情了,如给女优脱衣。

如果你还记得「DeepNude」这款给女性脱衣的应用,此时你应该可以明白其背后技术了(利用Pix2Pix或CycleGAN理论上都可以实现DeepNude这类应用)。

声明:这对女性是极其不尊重的,也不是技术应该使用的地方,在本文「10.威胁」中会简单的讨论一下这类技术产生的风波。

CycleGAN已经可以比较好的实现双域图像的转换的,那如何比较好的实现多域图像的转换呢?可以搜索阅读StarGAN相关的资料,因为与本文主题无关,就不多讨论了。

下图是我通过StarGAN模型得到的效果,StarGAN可以实现图像的多域转换,下图的每一列表示不同的域不同域即不同的效果,其中分布是:原图、黑发、金发、褐色头发、反性化、老年化。

可以看出,早些时候在国外社交媒体火爆的FaceApp背后的技术其实也是GAN,将StarGAN完善一下,让模型具有工业级的参数规模(以及工业级的训练数据与算力支持),一个FaceApp就被弄出来了。

8.Faceswap-GAN换脸应用

前面讨论了这么多,是否已经有开源实现好的项目呢?

当然有,我们来看一下Faceswap-GAN,它是最初换脸项目deepfakes_faceswap的升级版。

deepfake_faceswap虽然实现了人脸替换,但还是有一些问题,比如原版中使用了dlib人脸识别库,该库在非「全脸」或脸比较偏的时候,人脸识别率就不高了,而Faceswap-GAN使用了MTCNN来作为人脸识别引擎,代价就是慢。

我们可以通过下面几张图片来理解Faceswap-GAN大致的实现思路,需要注意,Faceswap-GAN具体的实现细节与图中的流程并不完全相同。

从图中可以看出,在训练阶段,首先输入带有人脸的图像Person A,然后通过MTCNN人脸识别获得真脸图像 Real face A,接着将Real face A进行扭曲操作得到Warped face A(注意,扭曲只对人脸周围扭曲,不对人脸特征扭曲,如眼睛、鼻子等,这种做法与往图像上添加马赛克没有本质区别),然后通过自编码器将扭曲后的人脸图像还原,从而获得重建后的人脸 Reconstructed face A。

deepfake_faceswap项目使用了自编码器,但改进后的Faceswap-GAN通过GAN实现了相同的过程。

获得了Reconstructed face A后并没有结束,它还会获得人脸特征面具,图中称为Segmentation mask,人脸特征面具会与重建后的脸做运算,目的是只获取人脸的特征,特征外的其他部分不再需要,接着将人脸特征域用于扭曲后人脸Warped face A从而获得最终的结果Masked face A。

而在测试阶段,流程也是相同的,传入带有人脸的图像Person B,然后MTCNN识别人脸,随后直接将人脸传入,不需要进行扭曲造成,因为我们训练时,使用了Person A,此时使用Person B的真实人脸,自编码器会将其认为是扭曲后的人脸A,即 Warped A,此时会进行重建操作,然后再通过相同的方式将人脸特征面具与还原重构后的人脸运算获得仅需要的人脸特征部分,再与Real face B融合就可以获得最终的结果face B,但因为它的五官有face A的特点,所以看起来像face A。

Faceswap-GAN使用了3种不同的损失来训练整个神经网络,分别是重建损失(reconsturction loss)、对抗损失(Adversarial loss)与感性损失(Perceptual loss)。

重建损失:对比重建后的人脸与真实人脸之间的差距,具体而言,就是使用平均绝对误差(MAE)对图像中的每一个像素进行计算,希望随着训练,将这个损失降低到最小。

对抗损失:判别器判断数据是真实数据还是虚假数据,对于生成器而言,它希望判别器给它生成的数据标记为真实数据,而对判别器而言,它希望给生成器生成的数据标记为虚假数据,两者博弈产生的损失。

感性损失:用于改善生成图像中眼球的方向,使生成的图像更加真实,并且可以平滑处理生成图像中可能产生的伪影,该损失使用了VggFace模型(VggFace使用了VGG16实现人脸识别的模型)

Faceswap-GAN还使用了很多技巧来完善生成的数据,并且它还提供了可以在Google的colab上直接执行的代码,使得使用门槛进一步降低(colab最长只能运行12个小时,这份代码只能生成一个轻量的Faceswap-GAN)。

最后提一句:Faceswap-GAN背后采用的是CycleGAN。

9.一张图像实现视频换脸

聪明的读者可以发现了,前面的方式很酷,但似乎与「ZAO」的不一样,「ZAO」似乎只用上传一张图像就是实现换脸了。

比如Faceswap-GAN,想通过它进行换脸,就需要准备两个人的大量图像,然后经过一定时长的训练,从而实现两者的换脸,此时如果传入第三者的脸(未经过训练)进行换脸操作,效果是不好的。

那「ZAO」是如何只通过一张图像就实现换脸的呢?

具体我也不清楚,因为「ZAO」团队没有说是通过什么技术实现的,但可以确定,并不是利用AutoEncoder、Pix2Pix或CycleGAN之类的,即与DeepFace或Faceswap使用的技术不同。

虽然不知道「ZAO」如何实现,但想要实现这种的效果可以通过Meta-GAN的思路,即元学习+GAN。

在2019年的5月,三星给出了《Few-Shot Adversarial Learning of Realistic Neural Talking Head Models》论文。

在论文中,提供了通过少量图像甚至一张图像就可以实现人物换脸效果的思路,其中主要的就是元学习+GAN。

元学习简单而言就是学习如何学习,这涉及了大量的话题(本人对元学习所知也不多),本文不深入探讨,这里简单的讨论一下论文的大体思路:

  • 1.通过基于GAN的元学习,在大量的视频数据中训练获得模型
  • 2.训练完后,元学习会获得一个映射矩阵,元学习可以为GAN的生成器与判别器自动初始为适合目标人脸的参数,从而实现少量图像甚至一张图像就可以换脸的效果。

其模型的大致结构如下:

从论文描述可知,Embedder嵌入器会将头像以及面部标记数据都映射到嵌入向量中,该向量包含了与姿势无关的信息。

生成器会利用输入的面部标记数据去生成数据,生成器的卷积层会通过AdaIN获取嵌入向量中的信息(人脸特征信息)来生成人脸。

判别器由两步构成,一步是通过编码网络将三种图像编码为向量,然后与W矩阵相乘从而获得最终得分。

通过论文中的思路构建神经网络可以实现惊人的效果,比如通过一张蒙拉丽莎的图片,让她活过来给你讲故事。

10.威胁

这种技术的兴起也带来了坏的一面,如DeepNude(脱去女性衣服),此外国内很多人闻风而起,搭建了各种DeepFace网站,在降低使用技术门槛的同时,也更容易被一些心怀不轨的人利用。

这种技术在国外大多是被批评的,你无法想象,犯罪分子利用这种技术合成勒索视频给你父母、你的前任将你的脸合成到低俗视频中带来的影响。

人的脸不再属于自己是可怕的。

不要觉得受害的只是明星,我们要抵制技术使用到这种方面。

11.AI对AI,识破假视频

知道了一些换脸的技术以及明白了它会带来的危害,那如何识别假视频呢?

现在生成的视频通过肉眼已经难以分辨出真假了,难道只能坐以待毙?

下面介绍一下我看见的几种识别假视频的方法。

使用循环神经网络来识别视频

目前大多数生成视频都是利用DeepFace相关的技术,其背后就是AutoEncoder、Pix2Pix或者CycleGAN,但单纯的使用这一类技术实现人脸的替换会存在一些小问题,具体而言,就是视频的当前帧与前一帧之间是独立的,这样前一帧的一些重要信息就无法用于当前帧,当视频中画面光源有所变化时,通过这种方式生成的视频就会有「闪屏现象」,这里说的闪屏不是我们常说的闪屏,而是通过人眼难以观察到的像素异常变化。

此时训练一个模型来观察则可,如果视频中连贯的部分存在这种现象,则可能是生成的造假视频,这就需要视频时间维度上的信息(当前帧的画面受上一帧的影响)。

谈论到时间维度,自然会想到循环神经网络RNN、LSTM、GRU之类的,这里以LSTM作为代表来简单介绍一下。

RNN在时间维度较长的数据上使用容易出现梯度消失的现象,人话说就是RNN不适合处理太长的数据,比如一段话,一段话中的每个词都是与前一个词或后一个词是相关的,而RNN要处理一段话中比较后面的词汇时,容易「忘记」这段话中排的比较前的词汇(梯度消失),后来就提出了LSTM、GRU等模型来避免这类问题。

LSTM模型结构如下,其中t表示时间,本质就对数据进行运行,从而决定模型应该记住什么,应当忘记什么,最终让模型记住重要的信息。

LSTM常用于NLP领域,现在用于视频检测,其本质并没有改变,都是将当前时间节点之前的信息传递到当前时间节点。

更多细节可以参考中论文8。

通过眨眼生理信号来识别视频

通过标题就明白具体的识别方法了,如果是真实的视频,视频中的人物通常会有眨眼这种生理信号,而换脸后生成的虚假视频并不会有这样的生理特征。

此时可以利用CNN+LSTM的形式,通过判断视频图像中人物是否存在眨眼情况,来判断当前视频是真实视频还是生成视频。

论文中将这种方法称为LRCN方法。

其中(a)是原始的视频序列,(b)是做了面部对齐后的序列,LRCN方法会基于(b)中眼睛周围p1~6,这6个标签来提取特征、进行序列学习与进行眼睛状态的预测。

值得一提的是,LRCN方法并不是简单的判断视频中人像的眨眼次数,而是会通过视频每一帧中眼睛的状态来判断眼睛在下一帧是否会眨眼,比如人物在当前帧的眼睛是关闭的,那么在下一帧其睁开的概率就会比较大。

如下图,第一行是原始视频,存在眨眼,而第二行是生成的虚假视频,其中人物没有进行眨眼。

更多细节可以参考论文9。

使用肖像中的生物信息识别视频

简单而言就是利用视频中人脸的各种动作来捕捉其中的生物信息,而这些信息在生成视频中是不会存在的,或者是不符合规律的。

没细看论文,不多言,感兴趣可以参考论文10。

12.结尾

本文只是抛砖迎玉,很多细节并没有讨论,如果文中有不妥之处欢迎各位留言斧正,最后希望这些技术可以用到正途之上。

写作不易,如果喜欢,欢迎点好看。

13.参考

Paper

1.Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks:https://kpzhang93.github.io/MTCNN_face_detection_alignment/paper/spl.pdf

2.A Unified Multi-scale Deep Convolutional
Neural Network for Fast Object Detection:http://www.svcl.ucsd.edu/publications/conference/2016/mscnn/mscnn.pdf

3.Auto-Encoding Variational Bayes:https://arxiv.org/pdf/1312.6114.pdf

4.Generative Adversarial Nets:https://arxiv.org/pdf/1406.2661.pdf

5.Image-to-Image Translation with Conditional Adversarial Networks:https://arxiv.org/pdf/1611.07004.pdf

6.Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networkshttps://arxiv.org/pdf/1703.10593.pdf

7.Few-Shot Adversarial Learning of Realistic Neural Talking Head Models:https://arxiv.org/pdf/1905.08233.pdf

8.Deepfake Video Detection Using Recurrent Neural Networks:https://engineering.purdue.edu/~dgueraco/content/deepfake.pdf

9.In Ictu Oculi: Exposing AI Created Fake Videos by Detecting Eye Blinkinghttps://ieeexplore.ieee.org/document/8630787

10.FakeCatcher: Detection of Synthetic Portrait Videos using Biological Signalshttps://arxiv.org/pdf/1901.02212.pdf

数据集

1.WIDER FACE: A Face Detection Benchmark:http://shuoyang1213.me/WIDERFACE/

2.Deep Convolutional Network Cascade for Facial Point Detection:http://mmlab.ie.cuhk.edu.hk/archive/CNN_FacePoint.htm

代码

1.MTCNN_face_detection_alignment:https://github.com/kpzhang93/MTCNN_face_detection_alignment

2.pix2pixhttps://phillipi.github.io/pix2pix/

3.faceswap-GAN:https://github.com/shaoanlu/faceswap-GAN

简介

数据可视化是让我们感知数据的一种重要手段,通过不同的数据可视化的方式,使得我们可以在不同维度去理解当前的数据。

数据可视化的基本原理就是,人脑对色块的敏感性远大于数字,从演化论角度来讲,可以很好的分辨出不同颜色事物的祖先更容易活下来,而数字,那个时候并没有,所以进化而来的大脑并不擅长处理数字,进而对干巴巴的数据不敏感。

本章内容会提供代码以及相应的数据,公众号回复:Data1 则可获得。

Matplotlib简单概念

Matplotlib是Python中用于绘制二维图形的知名第三方库(如果要绘制三维图形,需要额外安装一些支持包),也是很多几天高层次数据可视化第三方库的基础支持库。

Matplotlib中绘制的图有下面集中元素:

解释一下:

  • Major tick:主线
  • Minor tick:线上的刻度
  • Major tick label:主线上的标签
  • Title:整个图的标题
  • Legend:标注
  • Y axis label:Y轴的标签
  • Line:绘制的线
  • Grid:网格
  • Markers:标记
  • Figure:图形
  • Axes:轴域

上图就是Matplotlib绘制的整个窗口,图中包含了实际图表、x轴、y轴以及每个轴对应的标题、刻度和标签。在Matplotlib中可以为图形添加多个轴域,具体而言,就是使用pyplot来创建多个轴域并改变其形状。

这里可能会疑惑,Figure、Axes与Axis之间有什么关系?特别是Axes与Axis,英文直译都称为轴,可以通过一张图解释三者的关系。

绘制图像的常见步骤

大多数时候,使用 Matplotlib 绘制数据的流程是类似的,虽然有些特殊的图像绘制需要一下特殊的操作,但大体流程都相似

  • 1.通过Pandas将要绘制图像的数据读入,如pd.read_csv()读入csv文件数据、pd.read_excel()读取Excel文件数据
  • 2.导入 Matplotlib , 具体为: import matplotlib.pyplot as plt
  • 3.使用 plt.plot() 绘制折线图,不同的图使用不同的绘图函数,所有的绘图函数都需要传入相应的数据
  • 4.使用plt.xlabel与plt.ylabel定义x轴与y轴的标签,如定义标签字体样式、字体大小、字段位置等待,如果不使用,Matplotlib就会使用默认的样式将要显示的内容在标签处显示。需要注意的是,默认的样式是不支持显示中文的,如果此时你的标签要显示的内容是中文,那么Matplotlib生成的图像中,标签位置对应的内容会成为一个空方块,要显示中文,需要指定字体。
  • 5.使用plt.xticks与plt.yticks定义x轴与y轴上的标记点,如定义标点的间隔
  • 6.使用plt.legend()标注,如折线图中有3条不同颜色的折线,通过legend()方法就可以标注出不同折线的含义
  • 7.使用plt.title()定义图中的标题
  • 8.使用plt.show()将最终的图像展示出来。

接着,就来看一些具体图像的绘制

直方图

直方图是我们比较常见的简单图像,它有助于我们理解数据的范围以及加强我们对数据的整体感知。

这里以商店销售特定游戏数据为例来绘制直方图,先来看一下数据的样子

1
2
3
4
5
6
7
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd

np_data=pd.read_csv('datasets/national_parks.csv')
print(np_data.head()) # 打印前5行数据

前5行数据如下:

绘制直方图的代码如下:

1
2
3
4
5
6
7
8
9
10
n, bins, patches = plt.hist(np_data['GrandCanyon'],  # 某一类游戏的数据
facecolor='orange', # 直方图中矩形的颜色
edgecolor='blue', # 直方图中矩形边的颜色
bins=10 # 绘制多少个矩形
)
plt.show()

print('n: ', n) # 表示每个矩形在 y 轴的最大值
print('bins: ', bins) # 表示每个矩形在 x 轴的值
print('patches: ', patches) # 每个矩形对应的 patch 对象

效果如下:

其中打印的内容如下:

1
2
3
4
n:  [ 5.  9. 10.  2.  1.  9. 16.  2.  1.  2.]
bins: [1253000. 1753123.8 2253247.6 2753371.4 3253495.2 3753619. 4253742.8
4753866.6 5253990.4 5754114.2 6254238. ]
patches: <a list of 10 Patch objects>

绘制直方图时,可以将cumulative设置为true,此时绘制直方图时,会将每个矩形前的所有矩形的y轴值累加并加上当前矩形的y轴值,然后再绘制这个矩形,代码与效果如下:

1
2
3
4
5
6
plt.hist(np_data['GrandCanyon'], 
facecolor='orange',
edgecolor='blue',
bins=10,
cumulative=True) # 累加属性设置为True
plt.show()

此外可以通过range属性来定义你想要看的直方图范围,比如我只关心(2000000, 5000000)之间的数据,其他数据我不关心,就可以用上range,使用如下:

1
2
3
4
5
6
7
plt.hist(np_data['GrandCanyon'], 
facecolor='orange',
edgecolor='blue',
bins=10,
range=(2000000, 5000000))

plt.show()

效果如下,主要观察图中的x轴,x轴不会超出(2000000, 5000000)这个范围

饼图

饼图也是一种常见的图像,它可以帮助我们理解数据中某个部分占总体数据的比例,此外,饼图并不适合用于说明一下信息,首先依旧是读入用于绘制饼图的数据

1
2
t_mov= pd.read_csv('datasets/types_movies.csv')
print(t_mov)

数据如下:

绘制饼图的代码如下:

1
2
3
4
5
6
plt.pie(t_mov['Percentage'],  # 计算比例的具体数据
labels=t_mov['Sector']) # 对应的标签

plt.axis('equal')

plt.show()

效果如下:

如果想要定制饼图的颜色,直接通过colors参数,指定一组颜色则可,如下:

1
2
3
4
5
6
7
8
9
10
colors = ['darkorange', 'sandybrown', 'darksalmon', 'orangered','chocolate']

plt.pie(t_mov['Percentage'],
labels=t_mov['Sector'],
colors=colors,
autopct='%.2f')

plt.axis('equal')

plt.show()

效果如图:

此外饼图还能绘制成类似披萨的形态,即即让一些扇形不拼接在一起,指定explode参数则可,如下:

1
2
3
4
5
6
7
8
9
10
11
explode = (0, 0.1, 0, 0, 0)

plt.pie(t_mov['Percentage'],
labels=t_mov['Sector'],
colors=colors,
autopct='%.2f',
explode=explode)

plt.axis('equal')

plt.show()

效果如图:

时间序列线图

一些具有时间顺序属性的数据可以被称为时间序列数据,股票数据就是一种典型的时间序列数据,通过这些数据绘制一条时间序列线图可以帮助我们更好的了解数据随着时间的推移有什么变化,下面以部分美股的股票数据为例绘制一下时间序列线图。

简单看一下数据

1
2
3
4
stock_data = pd.read_csv('datasets/stocks.csv')
# 将其中的日期转为Pandas中的DataTime,会更方便处理
stock_data['Date'] = pd.to_datetime(stock_data['Date'])
stock_data.head()

接着来绘制一下时间序列线图,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fig = plt.figure(figsize=(10,6)) # 定义画布大小

# 多个轴域进行作图
ax1 = fig.add_axes([0, 0, 1, 1]) # 添加轴域
ax2 = fig.add_axes([0.05, 0.65, 0.5, 0.3]) # 添加轴域

# 设置标题
ax1.set_title('AAPL vs IBM(inset)')

# 第一个轴域进行绘图
ax1.plot(stock_data['Date'],
stock_data['AAPL'],
color='green')

# 第二个轴域进行绘图
ax2.plot(stock_data['Date'],
stock_data['IBM'],
color='blue')

plt.show()

上述代码中,为了比对两支不同的股票在相同时间段内的走势,这里调用了 add_axes([x0, y0, width, height]) 方法,其前两个参数用于定义轴域左上角的开始位置,而width与height用于定义轴域的大小,定义好了轴域后就可以在其上绘图了,效果如下:

通过与上面类似的方法,可以添加多个轴域绘制多个时间序列线图,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Figsize for width and height of plot
fig = plt.figure(figsize=(15,7))

fig.suptitle('Stock price comparison 2007-2017',
fontsize=20)

ax1 = fig.add_subplot(231)
ax1.set_title('MSFT')

ax1.plot(stock_data['Date'],
stock_data['MSFT'],
color='green')

ax2 = fig.add_subplot(232)
ax2.set_title('GOOG')

ax2.plot(stock_data['Date'],
stock_data['GOOG'],
color='purple')

ax3 = fig.add_subplot(233)
ax3.set_title('SBUX')

ax3.plot(stock_data['Date'],
stock_data['SBUX'],
color='magenta')

ax4 = fig.add_subplot(234)
ax4.set_title('ADBE')

ax4.plot(stock_data['Date'],
stock_data['ADBE'],
color='orange')

ax4 = fig.add_subplot(235)
ax4.set_title('NFLX')

ax4.plot(stock_data['Date'],
stock_data['NFLX'],
color='chocolate')

ax4 = fig.add_subplot(236)
ax4.set_title('ORCL')

ax4.plot(stock_data['Date'],
stock_data['ORCL'],
color='teal')

plt.show()

效果如下:

结尾

本篇文章介绍了一部分Matplotlib可视化数据的用法,在下一文章中会介绍Matplotlib绘制其他图的用法,记得关注HackPython,拜拜。

Python进阶:使用Matplotlib进行数据可视化(二)

简介

接着 使用Matplotlib进行数据可视化(一) ,继续使用 Matplotlib 绘制图像,公众号回复 data2 就可以获得本文章的代码与使用数据。

箱线图(BoxPlot)

箱线图(BoxPlot)也称箱须图(Box-whisker Plot),它利用数据中的五个统计量:最小值、第一四分位数、中位数、第三四分位数与最大值来描述数据的一种方法,如下图:

1.minimum(最小值)与maximum(最大值)是数据中的最小值和最大值

2.median(中位数):对数据排序,找到中间位置的数,称为中位数,如果中间位置有两个数,则相加再除以2,如有数字1,2,4,5,7,7,8,9,此时中位数为:(5+7)/2 = 6

3.lower quartile,也称为第一四分位数:它是数据排序后,中位数左边数据的中位数,如有数字1,2,4,5,7,7,8,9,此时第一四分位数为1,2,4的中位数,则为2

4.upper quartile,也称第三四分位数:它是数据排序后,中位数右边数据的中位数,如有数字1,2,4,5,7,7,8,9,此时第三四分位数为7,8,9的中位数,则为8

5.IQR(Inter Quartile Range),即第一四分位数到第三四分位数这一部分的数据,它估计了中间50%的数据,上图没有绘制出IQR

5.outlier,也称离群值,如果一个值小于(第一四分位数 - 1.5*IQR)或大于(第三四分位数 + 1.5*IQR),则称这个值为离群值

使用箱线图可以粗略的判断出数据是否具有对称性以及数据分布的离散程度,下面就来绘制一下。

一开始,依旧先读取数据并进行简单的处理

1
2
3
4
exam_data = pd.read_csv('datasets/exams.csv')
# 仅提取考试分数相关的信息
exam_scores = exam_data[['math score', 'reading score', 'writing score']]
exam_scores.head()

为了方便绘制boxplot,将数据转为numpy中的数组类型

1
exam_scores_array = np.array(exam_scores)

使用matplotlib绘制boxplot

1
2
3
4
5
6
7
8
9
10
11
12
13
colors = ['blue', 'grey', 'lawngreen']

bp = plt.boxplot(exam_scores_array, # 数据
patch_artist=True, # patch_artist设置为True,在后面才能设置不同的颜色
notch=True) # 显示是否有凹槽

for i in range(len(bp['boxes'])):
bp['boxes'][i].set(facecolor=colors[i]) # 设置颜色,前提是:patch_artist要设置为True
bp['caps'][2*i + 1].set(color=colors[i])

plt.xticks([1, 2, 3], ['Math', 'Reading', 'Writing'])

plt.show()

小提琴图(ViolinPlot)

小提琴图(ViolinPlot)用于显示数据分布以及概率密度,它结合了箱线图与密度图的特征。

形象如图:

95% confidence interval(95%的置信区间)在图中指的是延伸出来的黑色细线
Density Plot((数据分布)密度图)
Median(中位数)
Interquartile range(四分位数范围)
Split densities by category(按类别划分密度图)

使用绘制箱线图的数据绘制小提琴图

1
2
3
4
5
6
7
8
9
vp = plt.violinplot(exam_scores_array,
showmedians=True)

plt.xticks([1, 2, 3], ['Math', 'Reading', 'Writing'])

for i in range(len(vp['bodies'])):
vp['bodies'][i].set(facecolor=colors[i])

plt.show()

从图中可以看出,小提琴图的中间部分数据分布密度更大,这表明学生分数大部分都在平均水平附近

双轴图(TwinAxis Plot)

双轴图,顾名思义,就是一张图有两个y轴,当我们的数据使用相同x轴时,就可以考虑绘制双轴图。

​通过双轴图可以很直观的感受出两种数据之间的关联性,比如人口数据与国内生产总值数据在相同的时间轴(x轴)上,此时就可以用双轴图来判断两者的变化有没有关联性。

这里以Austin(奥斯汀,美国得克萨斯州的首府)城镇的天气数据来绘制双轴图,主要使用其中的平均气温与平均风速这两列数据,从而判断这两者有没有什么联系

首先,依旧是将数据读入,并取其中需要的数据

1
2
3
4
5
6
7
austin_weather = pd.read_csv('datasets/austin_weather.csv')
austin_weather.head()
# Data 日期
# TempAvgF 平均气温,华氏温度
# WindAvgMPH 平均风速,以英里/小时为单位
austin_weather = austin_weather[['Date', 'TempAvgF', 'WindAvgMPH']].head(5)
pritn(austin_weather)

使用这些数据绘制双轴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 # 创建子图
fig, ax_tempF = plt.subplots()

#fig=plt.figure(figsize=(12,6)) 可以实现相同效果
fig.set_figwidth(12)
fig.set_figheight(6)

# 设置x标签
ax_tempF.set_xlabel('Date')

ax_tempF.tick_params(axis = 'x',
bottom=False, # 禁用 ticks
labelbottom=False # 禁用 x 轴标签
)

# 设置左 Y 轴标签
ax_tempF.set_ylabel('Temp (F)',
color='red',
size='x-large')

# 为左 Y 轴标签设置labelcolor(标签颜色)与labelsize(标签大小)
ax_tempF.tick_params(axis='y',
labelcolor='red',
labelsize='large')

# 将 AvgTemp 绘制到 左 Y 轴上
ax_tempF.plot(austin_weather['Date'],
austin_weather['TempAvgF'],
color='red')

# 为两个图设置相同的x轴
ax_precip = ax_tempF.twinx()

#设置右 Y 轴标签
ax_precip.set_ylabel('Avg Wind Speed (MPH)',
color='blue',
size='x-large')

# 为右 Y 轴标签设置labelcolor(标签颜色)与labelsize(标签大小)
ax_precip.tick_params(axis='y',
labelcolor='blue',
labelsize='large')

# 将 WindAVg 绘制到 右 Y 轴上
ax_precip.plot(austin_weather['Date'],
austin_weather['WindAvgMPH'],
color='blue')

fig.legend(loc=1, bbox_to_anchor=(1,1), bbox_transform=ax_tempF.transAxes)

plt.show()

从图中可以看出,两者有些关系,但平均温度并不只受平均风速影响。

堆叠图(Stack Plot)

堆叠图是一种特殊的面积图,可以用来比较一个区间内的多个变量,与普通面积图不同,堆叠图每个数据面积的绘制起点都是基于前面一个数据面积来绘制的。

这里使用国家公园的数据来绘制堆叠图

1
2
np_data= pd.read_csv('datasets/national_parks.csv')
print(np_data.head())

national parks(国家公园)数据中有Badlands(荒废土地)、GrandCanyon(大峡谷)以及BryceCanyon(布莱斯峡谷)这3种类别的土地面积数据。

因为要绘制堆叠图,所以先要讲3中类别土地面积数据整合成一个二维数组,这里直接通过numpy的vstack()方法来实现这个效果,vstack()方法简单示例如下:

1
2
3
4
5
6
7
8
import numpy as np
a=[1,2,3]
b=[4,5,6]
print(np.vstack((a,b)))

输出:
[[1 2 3]
[4 5 6]]

接着就使用national parks数据来绘制一下堆叠图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
x = np_data['Year']
y = np.vstack([np_data['Badlands'],
np_data['GrandCanyon'],
np_data['BryceCanyon']])

# 每个面积区域的标签
labels = ['Badlands',
'GrandCanyon',
'BryceCanyon']

# 每个面积区域的颜色
colors = ['sandybrown',
'tomato',
'skyblue']

# 与 pandas 的 df.plot.area() 类似
# stackplot()创建堆叠图
plt.stackplot(x, y,
labels=labels,
colors=colors,
edgecolor='black')

# 绘制标注
plt.legend(loc=2)

plt.show()

百分比堆叠图

百分比堆叠图类似于普通堆叠图,只是每个数据被转换成了对应的百分比然后再绘制到图中,依旧使用national parks(国家公园)数据来绘制百分比堆叠图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plt.figure(figsize=(10,7))

# divide函数:在整数和浮点数除法中均只保留整数部分
data_perc = np_data.divide(np_data.sum(axis=1), axis=0)

plt.stackplot(x,
data_perc["Badlands"],data_perc["GrandCanyon"],data_perc["BryceCanyon"],
edgecolor='black',
colors=colors,
labels=labels)

plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))

plt.show()

效果如下:

结尾

本篇文章介绍了一部分Matplotlib可视化数据的用法,在下一文章中会介绍Matplotlib绘制其他图的用法,记得关注HackPython,拜拜。

简介

当我们完成一篇文章时,当然希望将自己的文章发布到多个不同的平台,让更多人看到,但自己一个个手动登录然后手动去发送实在太枯燥的,自己也有这样的需求,希望自己公众号的文章可以同步到知乎、CSDN等各种不同的平台上,找了一下市面上的工具,没有特别满意的,所以就着手自己开发一个。

现在已经开源到Github上:https://github.com/ayuLiao/AutoPublish

欢迎大家使用以及提PR

至于如何使用,Github的README.md中有比较详细的说明,这里就不再说明,本文主要从源码层面介绍一下AutoPublish

原理分析

平台登录

为了快速实现这个文章多平台发送的功能,选择使用Selenium来实现登录和文章发布的逻辑,但在实现过程中遇到了一些问题,如知乎、CSDN登录时都会检测到Selenium。

​在开发AutoPublish时主要使用 Chrome,这里尝试让 Selenium 通过开发者模式控制 Chrome,开发者模型下的Chrome在某些关键参数上会与正常使用相同,但是可惜的是依旧没有绕过检查,这说明这些网站使用了其他参数来判断你使用了Selenium,只是这个参数我们自身无法确定,遇到这种情况,有三种解决方法

  • 1.使用中间人 mitmproxy,将请求中的参数修改,这里可以将Selenium中所有的参数都修改了(推荐)
  • 2.编写Chrome插件,Selenium通过插件模式加载Chrome,让插件通过JS直接修改Chrome的参数(编写插件难度较大,本人未使用过)
  • 3.重新编译Selenium,替换关键变量名(难度大,本人未使用过)

但目前支持的3个平台(知乎、CSDN、豆瓣)都没有使用上面3种方式,而是直接使用requests通过模拟请求的方式来实现登录,其中知乎的登录规则最为恶心:(。

使用Selenium

完成登录后,会获得对应的Cookies,这些Cookies就类似于身份证一样的存在,有了正确的Cookies你就可以在登录状态做相应的事情了,比如在登录的账号下发表文章。

其实发表文章理论上也是可以使用requests的形式来实现的,但简单看一下,不同平台发送文章的逻辑不相同,图片处理、样式处理这些也比较棘手,为了最快速度的实现项目,依旧通过Selenium的形式去实现,简单快速。

​但在使用 Selenium 的过程中也遇到了一些问题,比如文章中图像的输入,像 CSDN 还好,因为后台就是 MarkDown 编辑器,样式这些不需要我们关心,而知乎需要自己将 MarkDown 渲染成 HTML,此时图像输入就比较麻烦了,此外 Selenium 不支持输入 emoji 表情,我的部分文章为了让读者不感觉到无趣,在部分段落中添加了 emoji,而 Selenium 发送输入包含 emoji 的文字时就会抛出异常。

使用autogui

最后决定使用autogui,autogui可以控制计算机的鼠标与键盘,从而实现点击、输入等效果,安装autogui需要安装相应的依赖驱动,这也操作了一些困扰,当autogui控制你鼠标或键盘时,你就无法使用鼠标或键盘了,因为它是通过驱动去控制的,与通过真实鼠标去控制是类似的,所以你使用autogui会暂时失去对鼠标或键盘的控制。

如果不在意暂时失去鼠标或键盘的控制,autogui就是非常好用了,直接通过控制键盘的方式,实现「键盘级」的复制粘贴,此时内容就会完全复制到不同平台相应编写内容的区域了,但这其实也隐藏着一个问题,就是操作对象必须获得了「焦点」,即浏览器要在所有窗口前,此时复制的内容才会被正确复制在浏览器对应的位置。

如果你使用PyCharm来运行该项目,此时代码无法完整的运行完,这是因为PyCharm无论是运行模式还是Debug模式,其实都占据了「焦点」,autogui复制的内容会出现在PyCharm光标处。

代码分析

Selenium二次封装

因为整个项目会经常使用Selenium,为了让代码更加简洁,这里以单例模式创建浏览器实例并将Selenium的常用方法了二次封装。

简单看一下单例模式的实现方式,代码如下,其实就是利用了__new__方法,先判断实例是否存在,不存在再创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Driver(object):
_instance = None
driver = None

# Singleton mode
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Driver, cls).__new__(cls, *args, **kwargs)
cls.driver = cls._instance.initdriver()
return cls._instance

def initdriver(self, plug=False, brower='Chrome'):
if brower == 'Chrome':
option = webdriver.ChromeOptions()
# Remove warnings from browser that 'Chrome is under the control of automated software'
option.add_argument('disable-infobars')
# No interface
# option.add_argument('headless')
driver = webdriver.Chrome(executable_path=CHROMEDIRVER, chrome_options=option)
driver.set_window_size(1200, 900)
elif brower == 'FireFox':
if plug:
# open %APPDATA%\Mozilla\Firefox\Profiles\ find firefox plugin,then load plugin configuration.
firefox_plug_dir = ''
profile_directory = os.path.join(appdata, 'Mozilla\Firefox\Profiles', firefox_plug_dir)
profile = webdriver.FirefoxProfile(profile_directory)
# Launch browser
driver = webdriver.Firefox(firefox_profile=profile)
return driver

接着看一下对Selenium的二次封装,比如一些点击的操作,为了确保操作对象的存在,这里会先隐式判断元素是否存在,如果存在再点击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@run_time
def waitxpath(self, xpath):
try:
WebDriverWait(self.driver, WAITTIME, 1).until(
lambda x: x.find_element_by_xpath(xpath)
)
except:
traceback.print_exc()
return ERROR, 'waif xpath 20s timeout, try again'
return SUCCESS, 'wait xpath finish'

@run_time
def choice_select(self, xpath, content, sleeptime=0):
'''
Choick select element
:param xpath: select xpath
:param content: select value
'''
self.waitxpath(xpath)
select = Select(self.driver.find_element_by_xpath(xpath))
# select the value of text='xxx'
select.select_by_visible_text(content)
if sleeptime:
time.sleep(sleeptime)

tkinter编写界面

为了方便使用,我还是 tkinter 编写了一个「很丑」的界面,之所以选择 tkinter 是因为它是 Python 的内置库,使用起来也比较简单。

使用 tkinter 制作界面有个关键点,就是不能让负责界面渲染的主线程执行耗时操作,不然,界面就会出现严重的卡顿现象,常规的做法就是开启一个新线程来负责耗时逻辑,然后通过全局变量在不同的线程之间传递数据,这个全局变量通常会定义为队列。

在登录不同平台时,如果遇到验证码也需要通过 tkinter 显示并获取验证码正确的值,此时可以使用 tkinter 的弹窗机制,弹出一个新窗口来显示验证码并获取验证码的真实值,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PopUpCaptchWindow(object):
'''
弹出验证码窗口
'''
def __init__(self,master, imgpath):
'''
:param master: 父窗口
:param imgpath: 图像路径(验证码图片路径)
'''
top=self.top=Toplevel(master)
self.l=Label(top,text="请输入验证码:")
self.l.pack()

im = Image.open(imgpath)
img = ImageTk.PhotoImage(im)
panel = Label(top, image=img)
panel.image = img
panel.pack(expand="yes")

self.e=Entry(top)
self.e.pack()
self.b=Button(top,text='确定',command=self.cleanup)
self.b.pack()
def cleanup(self):
self.value=self.e.get()
self.top.destroy()

AutoPublish 本质其实是与不同平台做对抗,如果平台登录规则变动或发送文章的规则变动,AutoPublish 的逻辑就会失去效果,这不只是 AutoPublish 会面临的问题,任何爬虫项目都会面对这样的问题。

​希望感兴趣的朋友为 AutoPublish 点个星,此外也欢迎关注 HackPython

最后再提一下,项目地址:https://github.com/ayuLiao/AutoPublish

译自:https://medium.com/coinmonks/visualizing-brain-imaging-data-fmri-with-python-e1d0358d9dba

简介

大脑是人类目前所知的最复杂的器官,为了很好的了解大脑这个器官,我们做了很多努力,核磁共振成像(Magnetic Resonance Image,MRI)技术就是其中的重要突破,通过MRI的方式,我们可以获得大脑的一些数据。

近年来,随着机器学习的兴起,医学数据与机器学习结合使用的情况越来越多,而要有效的使用好医学数据,其前提就是处理好这些数据,本文内容会重点介绍如何使用Python来处理与分析这些脑成像数据,不会涉及过多医学知识。

sMRI与fMRI

脑成像相关的数据可以去SPM网站中下载,SPM的含义是统计参数映射(Statistical Paramtric Mapping),MRI生成的数据其实就是一种参数映射数据,当然,更加方便的是在工作公众号中回复data3获得相应的数据与jupyter代码文件。

下载后,其中有4个文件,其中README开头的为描述文件,fM00223为功能性核磁共振(funciton MRI,fMRI)成像数据,sM00223为结构性核磁共振(structural MRI, sMRI)成像数据。通过描述文件可知,这些数据是一个人躺在MRI机器上听「双音节词」时大脑的成像数据。

为了方便理解后面数据处理的内容,有必要理解sMRI与fMRI是什么以及两者的差异。

结构性核磁共振(sMRI)

因为人的体内存有大量的水分子,而水分子中还有氢原子,sMRI其实就是利用氢原子来成像,这意味着人身体中的内脏、软组织等含有高水分与脂肪的器官会被清楚的扫描出来,而大脑就是这样的一个器官,通过sMRI可以清晰的看到大脑中的密集结构与大量细节,但sMRI的成像无法观察到大脑的运动情况,即无法判断那些部位目前是比较活跃的,只能给出大脑的结构细节。

如下图,科学家利用sMRI对人体腹腔进行成像,从图可以看出,腹腔的结构很明显。

功能性核磁共振(fMRI)

为了弥补sMRI的缺陷,fMRI应运而出,fMRI主要通过血氧浓度水平依赖(Blood-oxygen-level dependent,BOLD)来成像,一个器官的某个部位活跃,此时这个器官的这个部位就需要消耗更多的氧气,以此为依据,来进行成像。

通过fMRI的方式,我们可以很好的判断出大脑此时那些区域是活跃的,但这种活动并不等同于神经活动,但fMRI也有缺陷,即它的成像会损失大量的细节。

如下图,科学家通过fMRI,利用BOLD探索大脑活跃区域。

但从图中可以看出,大脑的细节几乎看不清晰,所以目前常规的方式是使用sMRI对大脑结构进行成像,而fMRI对大脑活跃区域进行成像。

sMRI数据可视化处理

通常,神经影像文件都会以相应的规律将数据存储在固定文件格式的文件中,我们可以通过NiBabel库来读/写常见的神经影像文件中的数据。

sMRI对应的数据在sM00223文件夹下,进入文件夹可以发现有两种不同文件格式的数据,分别是.img与.hdr,这其实是医学影像领域常见的格式,主要用于「分析」,其中,.img作为数据文件,包含二进制的图像资料,而.hdr作为头文件,包含图像的元数据,但这两种格式现在逐渐被.nifti格式代替,这是因为.hdr头文件难以完全真实反映元数据。

使用NiBabel将sMRI获得的数据载入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import nibabel

# sM0223对应的文件
data_path = './fMRI_data/sM00223/'
files = os.listdir(data_path)

# 读取其中的数据
data_all = []
for data_file in files:
if data_file[-3:] == 'hdr':
data = nibabel.load(data_path + data_file).get_data()

# 打印数据维数
print(data.shape)

# -------结果
(256, 256, 54, 1)

可以看出,sMRI产生的是4维数据,但第4维其实没有包含任何信息,其说明了sMRI每次扫描会产生54个数据切片,每个切片对应图像的大小为256x256个体素。

体素:类似像素,像素表示的是二维图像的最小单位,而体素则用于三维成像空间。

为了方面可视化每个切片的数据,可以简单处理一下数据:

1
2
3
4
5
6
7
import numpy as np

data = np.rot90(data.squeeze(), 1)
print(data.shape)

# -------结果
(256, 256, 54)

上述代码中,先通过numpy.squeeze()删除了数组中的单维条目,此时无用的第四维会被删除,接着使用numpy.rot90()将数组逆时针旋转了90度。

简单处理后,直接使用Matplotlib对每10个切片中的一个切片进行数据的绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt

# 创建 6 个子图,不清楚其中概念,可以看本公众号关于Matplotlib的文章
fig, ax = plt.subplots(1, 6, figsize=[18, 3])

n = 0
slice = 0
for _ in range(6):
ax[n].imshow(data[:, :, slice], 'gray')
ax[n].set_xticks([])
ax[n].set_yticks([])
ax[n].set_title('Slice number: {}'.format(slice), color='r')
n += 1
slice += 10

fig.subplots_adjust(wspace=0, hspace=0)
plt.show()

这就是通过sMRI数据绘制出的大脑结构图了,其中第0层切片是最低的一个(接近脖子位置),而第50层切片是最高的一个(接近头顶),在第20层,可以看具有眼睛的切片。

fMRI数据可视化处理

阅读README.txt可知fM00223数据集中,每张图像的大小为64x64个体素,采集的片数为64以及采集的卷数为96。知道了这些信息,就可以对fM00223数据集中的数据进行重构。

打开fM00223文件夹,可以发现确实正好有96个Hdr文件,这意味着每个文件包含了一个卷的所有片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# fMRI数据的基本信息
x_size = 64
y_size = 64
n_slice = 64
n_volumes = 96

# 获得所有文件
data_path = './fMRI_data/fM00223/'
files = os.listdir(data_path)

# 读取数据并进行重塑
data_all = []
for data_file in files:
if data_file[-3:] == 'hdr':
data = nibabel.load(data_path + data_file).get_data()
# 将所有数据添加到list中,从而多了一个维度:时间维度
data_all.append(data.reshape(x_size, y_size, n_slice))

接着就可以通过Matplotlib可视化展示数据了,因为组成这些数据的是体素,是三维的图像,我们无法直接看到所有的数据,所有进行切片操作,通常会横切大脑从而获得冠状面(coronal)、横切面(transversal)和矢状面(sagittal)这3个平面,理解这三个概念很重要,如下图:

  • 冠状面为左,右方向将人体纵切为前后两部分的断面
  • 横切面指横向水平切割的平面
  • 矢状面是指将躯体纵断为左右两部分的解剖平面

看一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 创建 3x6 的子图,大小为 18x11
fig, ax = plt.subplots(3, 6, figsize=[18, 11])

# 组织数据以冠状平面进行可视化,第四维度为时间维度,这里取第一个时间点
coronal = np.transpose(data_all, [1, 3, 2, 0])
coronal = np.rot90(coronal, 1)

# 组织数据以横切平面进行可视化
transversal = np.transpose(data_all, [2, 1, 3, 0])
transversal = np.rot90(transversal, 2)

# 组织数据以矢状平面进行可视化
sagittal = np.transpose(data_all, [2, 3, 1, 0])
sagittal = np.rot90(sagittal, 1)

# 可视化不同平面
n = 10
# 对每个切片的6个切面进行操作
for i in range(6):
ax[0][i].imshow(coronal[:, :, n, 0], cmap='gray')
ax[0][i].set_xticks([])
ax[0][i].set_yticks([])
if i == 0:
ax[0][i].set_ylabel('coronal', fontsize=25, color='r')
n += 10

n = 5
for i in range(6):
ax[1][i].imshow(transversal[:, :, n, 0], cmap='gray')
ax[1][i].set_xticks([])
ax[1][i].set_yticks([])
if i == 0:
ax[1][i].set_ylabel('transversal', fontsize=25, color='r')
n += 10

n = 5
for i in range(6):
ax[2][i].imshow(sagittal[:, :, n, 0], cmap='gray')
ax[2][i].set_xticks([])
ax[2][i].set_yticks([])
if i == 0:
ax[2][i].set_ylabel('sagittal', fontsize=25, color='r')
n += 10

fig.subplots_adjust(wspace=0, hspace=0)
plt.show()

感谢阅读,如果喜欢,点好看关注,如果有很想跟我说的话,欢迎直接跟我对话,个人微信:sighblue。

原文:https://www.sinostep.com/from-5USD-hourly-job-on-upwork-to-million-dollar-cross-border-consulting-business/

简介

从2011年以来,我一直在Upwork上从事工作,一开始,我的入门策略是定比较低的价格:每小时5美元,现在我是中国为数不多的高收入者之一。

Upwork:一个全球性的自由职业平台,让企业和独立的专业人士可以进行远程连接与协作,Upwork原名为Elance-oDesk,2015年更名为Upwork。

在过去的5年里, 我被邀请到10多个国家(如:美国,法国,比利时,墨西哥,泰国)进行商务旅行,我真的很喜欢这种工作方式(既能旅行,又能结交好友开拓眼界,还能赚钱,我感觉没人会不喜欢☺)

当我在Upwork赚了大约10万美元后,我自己的咨询业务也建立了,从此我不再单纯的依赖于自由职业者的市场。

当然,有时候我依然会从Upwork中找一些有趣的项目赚取一些零花钱。

我被很多网站、杂志和公司采访,现在越来越多国人知道我的故事。

我也被邀请到了知乎,经营一个关于自由职业者和创业公司的专栏,去年的11月,我被邀请到知乎的在线圆桌会议分享我对自由职业的见解和对中国跨境自由职业者的建议。

我在Upwork的第一份工作

我花了好多年才找到我的第一份工作,这是真的,我并没有开玩笑。

首先,我的母语并非英语,所以我很难深刻的理解Upwork这个平台中的游戏规则。

其次,我也怀疑Upwork是否可以成为自由职业者的金矿,如中国最大的自由职业市场:猪八戒就给了我非常差的印象,因为大多数人靠它根本无法过上体面的生活。

当我注册好账号后,一开始,我把自己的时薪定为30美元,然后发了一些提案,并相应收到了一些回复,但竞争实在太激烈了,我根本找不到工作。

在那个时候,我并不认为自己有某些很特殊的才能,我很难说服客户点击「租用」按钮。

后来,我开始发一些关于英文翻译与网站设计相关工作的提案,但我的竞争对手无处不在,尤其是来自与印度和菲律宾的竞争者。

让我感到荒谬的是,一个印度人会以一个很低的价格从西方客户那里获取英汉翻译的工作,然后再以更低的价格把这些工作外包给一个中国自由翻译者。

我并不想参与这样的游戏,但我真的很想知道那些成功的自由职业者到底是怎么赚到这么多钱的?

我改变了主意。

首先,我要学习Upwork是如何工作的,然后我才能靠它谋生。

我选择了最简单易行的策略:降低我的价格。

我在Upwork的第一份工作就是从Upwork获取快照资料,然后写出相应的内容,是的,我从一个德国客户手里开始了这项工作,每小时5美元。

这是一次非常愉快的经历,因为我才降低了一周的价格,它就对我产生作用了,这给了我很大的信心,让我愿意花更多时间在Upwork上。

第二份工作、第三方工作、更多的工作…以及我时薪的增长

当你得到自己的第一份工作后,你就在前10%之列了

大约90%在Upwork上的自由工作者其实没有从这个平台上获取过一分钱的收入,直到你接到第一单后,你才算真正的开始。

我很高兴我自己能接到第二份工作、第三份工作以及更多的工作。

我为自己定制了一个定价策略:每个月才去增加你的时薪,而不是每周

我就是这样做的,我从2011年11月起,每小时收费20美元,然后再2012年底,我每小时收费35美元,到2014年3月,我每小时收费变为了55美元,接着到2017年2月,我已经将我每小时的收费变为了300美元。

规划我的咨询业务

如果你能平衡好自己的工作与生活,增加时薪就会让你工作的更少而赚得更多。

如果你想过上体面的自由职业生活,你必须在早期阶段就进行一些长远的思考。我从Upwork获得第一份工作的那一天起就开始思考建立起自己的咨询业务了。

这看起来很有希望,因为我们可以发现有很多潜在客户,但我也知道,这是一条崎岖不平的道路,可是值得一试。

于是我通过WordPress建立了一个网站,并开始写博客。

初始问题

我每天都会问自己以下的的问题

  • 他们有什么问题需要解决?
  • 为什么他们会有这些问题?
  • 我怎样才能帮助他们?
  • 我应该准备什么?
  • 如何说服他们相信我?
  • 其中有趣的部分是什么?

不仅仅只是工作,还要研究与学习

实话说,当我在Upwork上接受越来越多工作时,我过的非常忙碌。

如果你已经有了一个既定的个人档案,当你将时薪调整到25~35美元时,不必太多担心,因为许多客户都是负担的起这样的定价的。

制作一个吸引人的个人资料

  • 1.让你的资料对潜在客户具有吸引力
  • 2.列出你引以为豪的成功案例
  • 3.使你的个人资料易于阅读

为你的客户思考

  • 1.当你以客户的角度去思考问题时,你就能赢得他们的心。
  • 2.详细说明你的技能以及你如何解决他人的问题

脱颖而出

记住,只有10%的自由职业者才能在市场上找到工作,你需要去研究那些成功找到工作的人以及那些普通的人,比较他们的差异。

随时改善你的个人资料,让他人阅读并得到他们的反馈

尽快找到你的第一份工作

  • 1.只有当你找到第一份工作时,你才算真正开始了自由职业的旅程。
  • 2.你要认真对待第一份工作的每一个建议。
  • 3.你可以告诉他们(雇主)你的情况,甚至考虑给他们一些折扣。
  • 4.即使第一个工作是一个简单的工作,也算是一个好的开始。

变得更好的秘诀

当你得到越来越多工作时,你需要思考一下如何才能做得更好,这里有一些有用的建议。

永远不要停止学习,去发现自己的潜力

你需要去尝试不同的可能性,去与不同的客户见面,去接受挑战。

你可以从实际的工作中学到很多东西,这些东西可能是此前从未尝试过的。

找到最需要的技能并去学习他们。

找到你的利基市场

经过各种各样的试验和测试后,你会发现你自己喜欢做什么工作?你自己的市场在哪里?

精心雕琢你的英语写作

大多数职业者的工作需要远程交流,英语写作和口语能力都很重要,特别是写作能力。

学习如何使你的英语写作变得更具有说服力,写出更好的建议、档案、电子邮件、报告等等。

这不是一件容易的事情,但值得你花时间去做。

你的说服力就是你的武器和最好的推销员。

建立你的个人品牌

使用你的真实姓名

我不明白为什么有些自由职业者喜欢上传假的照片,或者假装自己是一个很有吸引力的女孩来赢得工作,这不是约会,而是建立你自己的生意。

我建议使用你的真实姓名和专业的真实照片,不要再用昵称以及卡通头像了。

谈到生意,信任是首要问题,只有当人信任你,他们才会去雇佣你。

好的评价

客户好的评价是你最好的广告,所以一定要尽力去赢得好的评价和推荐。

我承认,并不是所有客户都会给出真实的评价或意见,但大多数客户都是善良的,他们会给出真实的意见,从而帮你建立可靠的网络声誉,有了良好的声誉后,其他有需求的人就会来找你

分析你的经验和专业知识

不要隐藏你的专业知识,将它们写下来,并分享出来。

在我开始写博客后,我被邀请参加了各种会议,接受了采访,甚至写电子书和中国商业指南等,并且我因此认识了更多朋友,获得了更多的商业机会,这真的很棒。

经营自己的咨询业务

我从自由职业市场中获得了许多经验,这意味着我可以创建训练营来帮助新入行的朋友。我提高了我的写作和演讲技巧,发现了自己的潜力并找到了自己喜欢的利基市场。

提高我的写作和演讲能力

我通过日常使用来提高我的英语水平,我并不需要花费金钱去上某些课程。

对于非英语为母语的人而言,在电话或面对面的会议中讲英语是需要一定自信的,我们可以在工作中也尝试使用英语来解决这个问题。

这对我的中文写作和口语也起到了一定的帮助。

现在我每天都会写作,这已经成为了一种习惯。

我的潜力

我找到了很多有趣的工作,并学到了很多东西

我约到了许多好客户和有经验的商业顾问,与这些聪明的家伙一起工作可以帮助我进一步理解商业的本质。

现在我可以做很多我以前不知道的事情。

我自己的小众市场

中国市场是如此有利可图,这吸引了越来越多海外公司计划进入中国市场。

然而,许多中小企业和初创企业发现很难在中国选择适合的利基市场,面对文化与语言上的障碍,也难以制定出合适的策略,而一些跨国咨询公司(这类公司不多)就可以为此提供服务,而这也是我的机会。

这篇文章后面的一些部分没有进行翻译,因为是作者介绍自己在跨国咨询这一领域提供的服务,如果想全面的了解,可以阅读原文。

文章中虽然没有非常细致的细节,但也看出了一个大致的路径。

先降低自己的起点,从小需求开始做,然后再慢慢成长并提高自己的价格,在这一过程中,你会积累一定的声誉,从而有了更广泛的信任基础,然后慢慢的再转到自己喜欢并擅长的领域。

简介

Flask是Python中有名的轻量级同步web框架,在一些开发中,可能会遇到需要长时间处理的任务,此时就需要使用异步的方式来实现,让长时间任务在后台运行,先将本次请求的响应状态返回给前端,不让前端界面「卡顿」,当异步任务处理好后,如果需要返回状态,再将状态返回。

怎么实现呢?

使用线程的方式

当要执行耗时任务时,直接开启一个新的线程来执行任务,这种方式最为简单快速。

通过ThreadPoolExecutor来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from flask import Flask
from time import sleep
from concurrent.futures import ThreadPoolExecutor

# DOCS https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor
# 创建线程池执行器
executor = ThreadPoolExecutor(2)

app = Flask(__name__)

@app.route('/jobs')
def run_jobs():
# 交由线程去执行耗时任务
executor.submit(long_task, 'hello', 123)
return 'long task running.'

# 耗时任务
def long_task(arg1, arg2):
print("args: %s %s!" % (arg1, arg2))
sleep(5)
print("Task is done!")

if __name__ == '__main__':
app.run()

当要执行一些比较简单的耗时任务时就可以使用这种方式,如发邮件、发短信验证码等。

但这种方式有个问题,就是前端无法得知任务执行状态。

如果想要前端知道,就需要设计一些逻辑,比如将任务执行状态存储到redis中,通过唯一的任务id进行标识,然后再写一个接口,通过任务id去获取任务的状态,然后让前端定时去请求该接口,从而获得任务状态信息。

全部自己实现就显得有些麻烦了,而Celery刚好实现了这样的逻辑,来使用一下。

使用Celery

为了满足前端可以获得任务状态的需求,可以使用Celery。

Celery是实时任务处理与调度的分布式任务队列,它常用于web异步任务、定时任务等,后面单独写一篇文章描述Celery的架构,这里不深入讨论。

现在我想让前端可以通过一个进度条来判断后端任务的执行情况。使用Celery就很容易实现,首先通过pip安装Celery与redis,之所以要安装redis,是因为让Celery选择redis作为「消息代理/消息中间件」。

1
2
pip install celery
pip install redis

在Flask中使用Celery其实很简单,这里先简单的过一下Flask中使用Celery的整体流程,然后再去实现具体的项目

  • 1.在Flask中初始化Celery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask
from celery import Celery

app = Flask(__name__)
# 配置
# 配置消息代理的路径,如果是在远程服务器上,则配置远程服务器中redis的URL
app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
# 要存储 Celery 任务的状态或运行结果时就必须要配置
app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/0'

# 初始化Celery
celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
# 将Flask中的配置直接传递给Celery
celery.conf.update(app.config)

上述代码中,通过Celery类初始化celery对象,传入的应用名称与消息代理的连接URL。

  • 2.通过celery.task装饰器装饰耗时任务对应的函数
1
2
3
4
@celery.task
def long_task(arg1, arg2):
# 耗时任务的逻辑
return result
  • 3.Flask中定义接口通过异步的方式执行耗时任务
1
2
3
@app.route('/', methods=['GET', 'POST'])
def index():
task = long_task.delay(1, 2)

delay()方法是apply_async()方法的快捷方式,apply_async()参数更多,可以更加细致的控制耗时任务,比如想要long_task()在一分钟后再执行

1
2
3
@app.route('/', methods=['GET', 'POST'])
def index():
task = long_task.apply_async(args=[1, 2], countdown=60)

delay()与apply_async()会返回一个任务对象,该对象可以获取任务的状态与各种相关信息。

通过这3步就可以使用Celery了。

接着就具体来实现「让前端可以通过一个进度条来判断后端任务的执行情况」的需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# bind为True,会传入self给被装饰的方法
@celery.task(bind=True)
def long_task(self):
verb = ['Starting up', 'Booting', 'Repairing', 'Loading', 'Checking']
adjective = ['master', 'radiant', 'silent', 'harmonic', 'fast']
noun = ['solar array', 'particle reshaper', 'cosmic ray', 'orbiter', 'bit']

message = ''
total = random.randint(10, 50)

for i in range(total):
if not message or random.random() < 0.25:
# 随机的获取一些信息
message = '{0} {1} {2}...'.format(random.choice(verb),
random.choice(adjective),
random.choice(noun))
# 更新Celery任务状态
self.update_state(state='PROGRESS',
meta={'current': i, 'total': total,
'status': message})
time.sleep(1)
# 返回字典
return {'current': 100, 'total': 100, 'status': 'Task completed!',
'result': 42}

上述代码中,celery.task()装饰器使用了bind=True参数,这个参数会让Celery将Celery本身传入,可以用于记录与更新任务状态。

然后就是一个for迭代,迭代的逻辑没什么意义,就是随机从list中抽取一些词汇来模拟一些逻辑的运行,为了表示这是耗时逻辑,通过time.sleep(1)休眠一秒。

每次获取一次词汇,就通过self.update_state()更新Celery任务的状态,Celery包含一些内置状态,如SUCCESS、STARTED等等,这里使用了自定义状态「PROGRESS」,除了状态外,还将本次循环的一些信息通过meta参数(元数据)以字典的形式存储起来。有了这些数据,前端就可以显示进度条了。

定义好耗时方法后,再定义一个Flask接口方法来调用该耗时方法

1
2
3
4
5
6
7
@app.route('/longtask', methods=['POST'])
def longtask():
# 异步调用
task = long_task.apply_async()
# 返回 202,与Location头
return jsonify({}), 202, {'Location': url_for('taskstatus',
task_id=task.id)}

简单而言,前端通过POST请求到/longtask,让后端开始去执行耗时任务。

返回的状态码为202,202通常表示一个请求正在进行中,然后还在返回数据包的包头(Header)中添加了Location头信息,前端可以通过读取数据包中Header中的Location的信息来获取任务id对应的完整url。

前端有了任务id对应的url后,还需要提供一个接口给前端,让前端可以通过任务id去获取当前时刻任务的具体状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@app.route('/status/<task_id>')
def taskstatus(task_id):
task = long_task.AsyncResult(task_id)
if task.state == 'PENDING': # 在等待
response = {
'state': task.state,
'current': 0,
'total': 1,
'status': 'Pending...'
}
elif task.state != 'FAILURE': # 没有失败
response = {
'state': task.state, # 状态
# meta中的数据,通过task.info.get()可以获得
'current': task.info.get('current', 0), # 当前循环进度
'total': task.info.get('total', 1), # 总循环进度
'status': task.info.get('status', '')
}
if 'result' in task.info:
response['result'] = task.info['result']
else:
# 后端执行任务出现了一些问题
response = {
'state': task.state,
'current': 1,
'total': 1,
'status': str(task.info), # 报错的具体异常
}
return jsonify(response)

为了可以获得任务对象中的信息,使用任务id初始化AsyncResult类,获得任务对象,然后就可以从任务对象中获得当前任务的信息。

该方法会返回一个JSON,其中包含了任务状态以及meta中指定的信息,前端可以利用这些信息构建一个进度条。

如果任务在PENDING状态,表示该任务还没有开始,在这种状态下,任务中是没有什么信息的,这里人为的返回一些数据。如果任务执行失败,就返回task.info中包含的异常信息,此外就是正常执行了,正常执行可以通task.info获得任务中具体的信息。

这样,后端的逻辑就处理完成了,接着就来实现前端的逻辑,要实现图形进度条,可以直接使用nanobar.js,简单两句话就可以实现一个进度条,其官网例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var options = {
classname: 'my-class',
id: 'my-id',
// 进度条要出现的位置
target: document.getElementById('myDivId')
};

// 初始化进度条对象
var nanobar = new Nanobar( options );

nanobar.go( 30 ); // 30% 进度条
nanobar.go( 76 ); // 76% 进度条

// 100% 进度条,进度条结束
nanobar.go(100);

有了nanobar.js就非常简单了。

先定义一个简单的HTML界面

1
2
3
<h2>Long running task with progress updates</h2>
<button id="start-bg-job">Start Long Calculation</button><br><br>
<div id="progress"></div>

通过JavaScript实现对后台的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 按钮点击事件
$(function() {
$('#start-bg-job').click(start_long_task);
});

// 请求 longtask 接口
function start_long_task() {
// 添加元素在html中
div = $('<div class="progress"><div></div><div>0%</div><div>...</div><div>&nbsp;</div></div><hr>');
$('#progress').append(div);

// 创建进度条对象
var nanobar = new Nanobar({
bg: '#44f',
target: div[0].childNodes[0]
});

// ajax请求longtask
$.ajax({
type: 'POST',
url: '/longtask',
// 获得数据,从响应头中获取Location
success: function(data, status, request) {
status_url = request.getResponseHeader('Location');
// 调用 update_progress() 方法更新进度条
update_progress(status_url, nanobar, div[0]);
},
error: function() {
alert('Unexpected error');
}
});
}

// 更新进度条
function update_progress(status_url, nanobar, status_div) {
// getJSON()方法是JQuery内置方法,这里向Location中对应的url发起请求,即请求「/status/<task_id>」
$.getJSON(status_url, function(data) {
// 计算进度
percent = parseInt(data['current'] * 100 / data['total']);
// 更新进度条
nanobar.go(percent);

// 更新文字
$(status_div.childNodes[1]).text(percent + '%');
$(status_div.childNodes[2]).text(data['status']);
if (data['state'] != 'PENDING' && data['state'] != 'PROGRESS') {
if ('result' in data) {
// 展示结果
$(status_div.childNodes[3]).text('Result: ' + data['result']);
}
else {
// 意料之外的事情发生
$(status_div.childNodes[3]).text('Result: ' + data['state']);
}
}
else {
// 2秒后再次运行
setTimeout(function() {
update_progress(status_url, nanobar, status_div);
}, 2000);
}
});
}

可以通过注释阅读代码整体逻辑。

至此,需求实现完了,运行一下。

首先运行Redis

1
redis-server

然后运行celery

1
celery worker -A app.celery --loglevel=info

最后运行Flask项目

1
python app.py

效果如下:

Flask异步运行任务的常见方式就介绍完了,因为本人在开发一个用于自动生成字幕的小玩具,其中视频上传以及字幕生成都是耗时任务,所以就单独写一篇文章来介绍一下这部分的内容,后面会将小玩具的代码开源让大家学习一下,先一睹其真容:

如果你觉得文章有帮助,请按一下右下角的「在看」小星星,那是可以按的,谢谢。