Dash Python 教程(3): 更多的关于用户界面布局

dash_html_components 定义了所有的HTML元素,这包括了这些HTML元素的所有属性,比如CSS风格属性。

比如我们可以定义一些inline CSS属性:

colors = {
    'background': '#111111',
    'text': '#7FDBFF'
}

app.layout = html.Div(style={'backgroundColor': colors['background']}, children=[
    html.H1(
        children='Hello Dash',
        style={
            'textAlign': 'center',
            'color': colors['text']
        }
    ),

    html.Div(children='Dash: A web application framework for Python.', style={
        'textAlign': 'center',
        'color': colors['text']
    }),

    dcc.Graph(
        id='example-graph-2',
        figure={
            'data': [
                {'x': [1, 2, 3], 'y': [4, 1, 2], 'type': 'bar', 'name': 'SF'},
                {'x': [1, 2, 3], 'y': [2, 4, 5], 'type': 'bar', 'name': u'Montréal'},
            ],
            'layout': {
                'plot_bgcolor': colors['background'],
                'paper_bgcolor': colors['background'],
                'font': {
                    'color': colors['text']
                }
            }
        }
    )
])

这个例子我们修改了Div和H1的 CSS 风格,比如:html.H1(‘Hello Dash’, style={‘textAlign’: ‘center’, ‘color’: ‘#7FDBFF’}) 渲染成
<h1 style=”text-align: center; color: #7fdbff;”>Hello Dash</h1>
要注意的是style风格定义和HTML的style定义稍有不同, 比如css中class在Dash中使用className, 属性的名称使用camelCased,比如 text-align 代替textAlign.

dash_core_components 中的一个Graph可以用来显示各种图表,它是基于plotlt.js开发,支持多达35种不同样式的图表:

df = pd.read_csv('https://gist.githubusercontent.com/chriddyp/5d1ea79569ed194d432e56108a04d188/raw/a9f9e8076b837d541398e999dcbac2b2826a81f8/gdp-life-exp-2007.csv')


app.layout = html.Div([
    dcc.Graph(
        id='life-exp-vs-gdp',
        figure={
            'data': [
                dict(
                    x=df[df['continent'] == i]['gdp per capita'],
                    y=df[df['continent'] == i]['life expectancy'],
                    text=df[df['continent'] == i]['country'],
                    mode='markers',
                    opacity=0.7,
                    marker={
                        'size': 15,
                        'line': {'width': 0.5, 'color': 'white'}
                    },
                    name=i
                ) for i in df.continent.unique()
            ],
            'layout': dict(
                xaxis={'type': 'log', 'title': 'GDP Per Capita'},
                yaxis={'title': 'Life Expectancy'},
                margin={'l': 40, 'b': 40, 't': 10, 'r': 10},
                legend={'x': 0, 'y': 1},
                hovermode='closest'
            )
        }
    )
])

Markdown支持

markdown_text = '''
### Dash and Markdown

Dash apps can be written in Markdown.
Dash uses the [CommonMark](http://commonmark.org/)
specification of Markdown.
Check out their [60 Second Markdown Tutorial](http://commonmark.org/help/)
if this is your first introduction to Markdown!
'''

app.layout = html.Div([
    dcc.Markdown(children=markdown_text)
])

 

其它一些常用的UI部件

app.layout = html.Div([
    html.Label('Dropdown'),
    dcc.Dropdown(
        options=[
            {'label': 'New York City', 'value': 'NYC'},
            {'label': u'Montréal', 'value': 'MTL'},
            {'label': 'San Francisco', 'value': 'SF'}
        ],
        value='MTL'
    ),

    html.Label('Multi-Select Dropdown'),
    dcc.Dropdown(
        options=[
            {'label': 'New York City', 'value': 'NYC'},
            {'label': u'Montréal', 'value': 'MTL'},
            {'label': 'San Francisco', 'value': 'SF'}
        ],
        value=['MTL', 'SF'],
        multi=True
    ),

    html.Label('Radio Items'),
    dcc.RadioItems(
        options=[
            {'label': 'New York City', 'value': 'NYC'},
            {'label': u'Montréal', 'value': 'MTL'},
            {'label': 'San Francisco', 'value': 'SF'}
        ],
        value='MTL'
    ),

    html.Label('Checkboxes'),
    dcc.Checklist(
        options=[
            {'label': 'New York City', 'value': 'NYC'},
            {'label': u'Montréal', 'value': 'MTL'},
            {'label': 'San Francisco', 'value': 'SF'}
        ],
        value=['MTL', 'SF']
    ),

    html.Label('Text Input'),
    dcc.Input(value='MTL', type='text'),

    html.Label('Slider'),
    dcc.Slider(
        min=0,
        max=9,
        marks={i: 'Label {}'.format(i) if i == 1 else str(i) for i in range(1, 6)},
        value=5,
    ),
], style={'columnCount': 2})

 

Dash Python 教程(2): 用户界面布局简介

Dash Web应用框架也可以算是一个MVC框架,虽然很多时候M, V, C都定义在一个文件中。本篇概述用户界面部分,Dash是一个纯Python应用框架,因此用户界面也是使用Python来描述的,Dash定义了两个 package:

import dash_core_components as dcc
import dash_html_components as html

另外也定义一个支持Bootstrap的UI部件:

import dash_bootstrap_components as dbc

dbc和dcc有部分重叠的UI 部件,它们之间大同小异。Dash的UI是基于HTML, React以及Dash自带的一些图形部件。因此如果你对HTML比较熟悉的话,Dash的UI界面布局看起来就比较熟悉了:

比如下面的例子:

app.layout = html.Div(children=[
    html.H1(children='Hello Dash'),

    html.Div(children='''
        Dash: A web application framework for Python.
    '''),

    dcc.Graph(
        id='example-graph',
        figure={
            'data': [
                {'x': [1, 2, 3], 'y': [4, 1, 2], 'type': 'bar', 'name': 'SF'},
                {'x': [1, 2, 3], 'y': [2, 4, 5], 'type': 'bar', 'name': u'Montréal'},
            ],
            'layout': {
                'title': 'Dash Data Visualization'
            }
        }
    )
])

 

这个界面布局使用了 Div ,H1, Graph元素来描述,显示如下:

  • UI 布局为一个由如Div,Graph组成的树形结构。
  • dash_html_components 包包含了所有的HTML标签,html.H1(children=’Hello Dash’) 组成生成一个HTML <h1>Hello Dash</h1>。
  • 并非所有的布局都是纯HTML元素,dash_core_components中包含了一些由Javascript,HTML和React生成的高层次的支持交互UI部件。
  • childern属性是一个比较特殊的属性,缺省情况下它是部件的第一个属性,意味着你可以省略这个属性名称,比如html.H1(‘Hello Dash’)和html.H1(children=’Hello Dash’)是等效的。childern属性可以是一个字符串,数值,另外一个UI组件或是一个组件列表。
  • 你的例子所使用的字体和上面的图中字体可能不同,你可以使用重新定使用Web应用所使用的CSS。
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = DjangoDash('BootstrapApplication',external_stylesheets=external_stylesheets)

 

Dash Python 教程(1): 概述

之前我们介绍了Open AI Gym,也提到了打算开发一个通用的Android游戏的Open AI Gym环境,在此之前,计划先开发一个澳大利亚股票市场交易的Open AI Gym的模拟环境,最近几天完成了对澳洲股票数据的处理https://github.com/asxgym/asx_data_daily 这个数据包含了近10年的历史数据,包括ASX股票指数。以及公司,板块数据。

Open AI Gym的模拟环境项目的框架asx_gym ,目前还只是一个框架,还在设计中。数据可以实现每日更新(后面介绍用法)

asx_gym 的 app目录为一个Django例子,可以用来显示股票数据,而Dash是一个非常实用的图像显示的软件包,用来显示股票数据再合适不可。Dash 本身可以作为独立的Web应用,也可以做为Django的一个模块,后面具体介绍Dash作为Django模块的配置,这里可以通过修改bootstrap_app.py

import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

from django_plotly_dash import DjangoDash

app = DjangoDash('BootstrapApplication')  # replaces dash.Dash

...

比如我们使用参考https://dash.plotly.com/layout 修改bootstrap_app.py如下:

app.layout = html.Div(children=[
    html.H1(children='Hello Dash'),

    html.Div(children='''
        Dash: A web application framework for Python.
    '''),

    dcc.Graph(
        id='example-graph',
        figure={
            'data': [
                {'x': [1, 2, 3], 'y': [4, 1, 2], 'type': 'bar', 'name': 'SF'},
                {'x': [1, 2, 3], 'y': [2, 4, 5], 'type': 'bar', 'name': u'Montréal'},
            ],
            'layout': {
                'title': 'Dash Data Visualization'
            }
        }
    )
])

 

Dash 本身是基于Flask, Plotly.js 和React.js. 但它本身确是一个纯Python的Web应用开发平台,特别适宜哪些使用Python语言进行数据处理的技术人员。

Open AI Gym教程(7): Gym 内置二维图形渲染系统

我们使用Open AI Gym 来测试人工智能或是机器学习的算法。尽管Gym以及Gym retro提供了数以千计的环境可供测试。但我们实际的问题总有gym没有提供的情形。因此Open AI Gym 通常有两种问题,一是如果把我们遇到的实际问题转化成Gym 场景,然后利用已有的Gym 机器学习的算法来解决实际问题二是如何选择和设计算法来解决Gym的场景。

我们很都人都玩过魔法,在没有完成三层之前,看到别人完成三层的时候,觉得好牛啊:-),实际情况并非如此。

实际上玩魔方,可能在完成第一层时,还可能需要动点脑筋,后面二层,三层基本上做为玩家来说,基本是套用口诀,判断当前魔方所处的状态,然后套用口诀,并没有任何智能可言。记得魔方所处的状态越多,恢复三层速度越快,这点和我们使用机器学习算法解决问题非常类似。

人工智能和机器学习目前是能够解决不少问题,当在实际应用时,可以除了少许研究人员来说,设计新的算法(类似于发现魔方新的口诀)还需要大量的脑力劳动,对于一般的应用开发。基本是也是套用现成的算法,判断下问题的性质,是supervised learning(监督学习),unsupervised learning(无监督学习) ,还是使用Reinforcement learning(强化学习)。可能在调整算法的超级参数(hyper parameters),还需要一些思考(就想玩魔方的第一层),后面基本也是套路。

机器学习的一个主要特点是无需事先设计好预定的算法,而是通过大量的数据来训练神经网络,而且很多时候数据量的多少可以弥补算法的不足,和我们常规的算法大不相同。

因此在解决实际问题时候,把问题构造成Open AI Gym场景相对显得更为重要,而且需要更多的”智能”。就像当初设计出魔方的人。

Gym的一个重要的方法是render,也就是显示出场景。可以是图像,内存(rgb_array)或是文字。

Gym的经典场景比如(cartpole),以及atari游戏的显示,Open AI Gym提供了一个比较简单的二维图形渲染系统,我们如果自定义场景也可以使用同样的图形系统作为显示:

gym.envs.classic_control.rendering

定义了这个图型系统,简单的说一个是矢量图像(Viewer),一个是光栅图像(SimpleImageViewer)显示,Atari游戏有自己的图像显示,因此直接使用SimpleImageViewer,显示图像imshow(image)。
Gym的经典场景使用矢量图像(Viewer),它提供一些简单的二维图像显示,比如点,线,面,坐标变换等:

下面的代码为CartPole的例子改写而来,旋转平衡杆720度。

from gym.envs.classic_control.rendering import *

length = 0.5

x_threshold = 2.4

screen_width = 600
screen_height = 400

world_width = x_threshold * 2
scale = screen_width / world_width
carty = 100  # TOP OF CART
polewidth = 10.0
polelen = scale * (2 * length)
cartwidth = 50.0
cartheight = 30.0

viewer = Viewer(screen_width, screen_height)

l, r, t, b = -cartwidth / 2, cartwidth / 2, cartheight / 2, -cartheight / 2
axleoffset = cartheight / 4.0
cart = FilledPolygon([(l, b), (l, t), (r, t), (r, b)])
carttrans = Transform()
cart.add_attr(carttrans)
viewer.add_geom(cart)
l, r, t, b = -polewidth / 2, polewidth / 2, polelen - polewidth / 2, -polewidth / 2
pole = FilledPolygon([(l, b), (l, t), (r, t), (r, b)])
pole.set_color(.8, .6, .4)
poletrans = Transform(translation=(0, axleoffset))
pole.add_attr(poletrans)
pole.add_attr(carttrans)
viewer.add_geom(pole)
axle = make_circle(polewidth / 2)
axle.add_attr(poletrans)
axle.add_attr(carttrans)
axle.set_color(.5, .5, .8)
viewer.add_geom(axle)
track = Line((0, carty), (screen_width, carty))
track.set_color(0, 0, 0)
viewer.add_geom(track)

l, r, t, b = -polewidth / 2, polewidth / 2, polelen - polewidth / 2, -polewidth / 2
pole.v = [(l, b), (l, t), (r, t), (r, b)]

cartx = screen_width / 2.0  # MIDDLE OF CART
carttrans.set_translation(cartx, carty)
poletrans.set_rotation(0)
rotate = 720

while rotate > 0:
    poletrans.set_rotation(rotate * math.pi / 180.0)
    rotate -= 1
    viewer.render()

viewer.close()

CartPole由Cart(Polygon),Pole(Polygon),Axis(Circle),Track(Line)构成,

这些简单的几何图像类型都是Geom 的子类,Geom可以定义几个属性,如颜色,线宽,坐标转换的Matrix(比如实现Pole的旋转)。Gym 图像显示是记忆pyglet(OpenGL)。如果你属性OpenGL,这些都是非常容易理解的。

Open AI Gym教程(6):自定义环境的基本步骤

虽然Open AI Gym自带了大量的环境可供测试,但我们终会碰到需要自定义环境的时候,本篇介绍构造自定义环境的基本步骤。

这里借用一个简单的例子,猜数字游戏,这个游戏和我们常见的猜数字类似,稍有不同。游戏随机给出一个浮点数(可以取值的范围事先不知道,但可以给出对于的值域空间Spaces),你可以最多猜200次,然后游戏根据你猜的数字,给出4中不同的测量值(Observation).

  • 0 — 初始,只在系统reset()后可以为0
  • 1 — 猜的数偏低
  • 2 — 猜的数和游戏给定的值相同,由于是浮点数,系统比较两个值偏差在1%内就认为相同。
  • 3 –猜的数偏大。

系统给出的奖励如下:

  • 1 如果猜中数字 ,允许1%偏差
  • 0 没有猜中, 也就是偏差高于1%

根据游戏规则,我们可以定义这个环境的Action 和 Observation 的值域空间,Observation比较简单,使用整数类型就可以

observation_space = spaces.Discrete(4)

而Action 可以用多种定义,最简单的是直接使用猜的数字作为值域空间,比如:

self.bounds = 10000

self.action_space = spaces.Box(low=np.array([-self.bounds]), high=np.array([self.bounds]),
                                       dtype=np.float32)
self.observation_space = spaces.Discrete(4)

此外,还可以使用Tuple类型, (Discrete(1),(Box(low=0, high=np.array([self.bounds]),
dtype=np.float32))
第一个增大或是减小,第二个为增大或是减小的数值。本例采用第一种方法。

根据上述的分析, 我们就可以来设计自定义Gym环境。一个Gym环境的项目结构如下:

.
├── LICENSE
├── README.md
├── guessing_number
│   ├── __init__.py
│   └── envs
│       ├── __init__.py
│       └── guessing_number_env.py
├── setup.py
└── test.py

其中setup.py 一般为:

from setuptools import find_packages, setup

setup(
    name="guessing_number",
    version="0.0.1",
    install_requires=["gym>=0.2.3", "numpy"],
    packages=find_packages(),
)

 

定义项目的名称,版本,以及所依赖的其它软件包。

而 guessing_number/__init__.py 一般如下:

from gym.envs.registration import register

register(id="GuessingNumber-v0", entry_point="guessing_number.envs:GuessingNumberEnv")

 

其中环境个名称的格式为GuessingNumber-v0,一般在环境名称后使用v0,v1来代表不同的版本。
entry_point给出环境人主类的人口点。

参考Open AI Gym教程(3): 环境 Env 我们可以定义GuessingNumberEnv如下:

import numpy as np

import gym
from gym import spaces
from gym.utils import seeding


class GuessingNumberEnv(gym.Env):
    """Number guessing game

    The object of the game is to guess within 1% of the randomly chosen number
    within 200 time steps

    After each step the agent is provided with one of four possible observations
    which indicate where the guess is in relation to the randomly chosen number

    0 - No guess yet submitted (only after reset)
    1 - Guess is lower than the target
    2 - Guess is equal to the target
    3 - Guess is higher than the target

    The rewards are:
    0 if the agent's guess is outside of 1% of the target
    1 if the agent's guess is inside 1% of the target

    The episode terminates after the agent guesses within 1% of the target or
    200 steps have been taken

    The agent will need to use a memory of previously submitted actions and observations
    in order to efficiently explore the available actions

    The purpose is to have agents optimise their exploration parameters (e.g. how far to
    explore from previous actions) based on previous experience. Because the goal changes
    each episode a state-value or action-value function isn't able to provide any additional
    benefit apart from being able to tell whether to increase or decrease the next guess.

    The perfect agent would likely learn the bounds of the action space (without referring
    to them explicitly) and then follow binary tree style exploration towards to goal number
    """
    def __init__(self):
        self.range = 1000  # Randomly selected number is within +/- this value
        self.bounds = 10000

        self.action_space = spaces.Box(low=np.array([-self.bounds]), high=np.array([self.bounds]),
                                       dtype=np.float32)
        self.observation_space = spaces.Discrete(4)

        self.number = 0
        self.guess_count = 0
        self.guess_max = 200
        self.observation = 0

        self.seed()
        self.reset()

    def seed(self, seed=None):
        self.np_random, seed = seeding.np_random(seed)
        return [seed]

    def step(self, action):
        assert self.action_space.contains(action)

        if action < self.number:
            self.observation = 1

        elif action == self.number:
            self.observation = 2

        elif action > self.number:
            self.observation = 3

        reward = 0
        done = False

        if (self.number - self.range * 0.01) < action < (self.number + self.range * 0.01):
            reward = 1
            done = True

        self.guess_count += 1
        if self.guess_count >= self.guess_max:
            done = True

        return self.observation, reward, done, {"number": self.number, "guesses": self.guess_count}

    def reset(self):
        self.number = self.np_random.uniform(-self.range, self.range)
        self.guess_count = 0
        self.observation = 0
        return self.observation

项目完成后可以使用

pip install -e .

来安装这个环境。

最有我们可以设计两个不同的Agent来玩这个猜数字游戏,一个是随机猜:

class RandomAgent(object):
    """The world's simplest agent!"""

    def __init__(self, action_space):
        self.action_space = action_space

    def act(self, observation, reward, done):
        return self.action_space.sample()

 

第二个是改进后的随机猜,根据step返回的observation (1-偏小,3-偏大)适当调整所猜的数字:

class BetterRandomAgent(object):
    """The world's 2nd simplest agent!"""

    def __init__(self, action_space):
        self.action_space = action_space

    def act(self, observation, last_action):
        new_action = last_action
        if observation == 1:
            new_action = last_action + abs(last_action / 2)

        elif observation == 3:
            new_action = last_action - abs(last_action / 2)
        if abs(last_action - new_action) < 1e-1:
            new_action = self.action_space.sample()
        return new_action

 

有了这两个Agent,我们就可以测试这个环境.分别运行100次,看看每个Agent猜中的次数,和平均猜的次数:

import gym

import guessing_number

class RandomAgent(object):
    """The world's simplest agent!"""

    def __init__(self, action_space):
        self.action_space = action_space

    def act(self, observation, reward, done):
        return self.action_space.sample()


class BetterRandomAgent(object):
    """The world's 2nd simplest agent!"""

    def __init__(self, action_space):
        self.action_space = action_space

    def act(self, observation, last_action):
        new_action = last_action
        if observation == 1:
            new_action = last_action + abs(last_action / 2)

        elif observation == 3:
            new_action = last_action - abs(last_action / 2)
        if abs(last_action - new_action) < 1e-1:
            new_action = self.action_space.sample()
        return new_action


if __name__ == '__main__':

    env = gym.make('GuessingNumber-v0')
    env.seed(0)
    agent = BetterRandomAgent(env.action_space)

    episode_count = 100
    reward = 0
    done = False

    total_reward = 0
    total_guesses = 0
    for i in range(episode_count):
        last_action = env.action_space.sample()
        ob = env.reset()
        while True:
            action = agent.act(ob, last_action)
            ob, reward, done, info = env.step(action)
            last_action = action

            # print(f'count={info["guesses"]},number={info["number"]},guess={action},ob={ob},reward={reward}')
            if done:
                total_reward += reward
                total_guesses += int(info["guesses"])
                break

    print(f'Total better random reward {total_reward}, average guess {round(total_guesses / 100, 1)}')

    env.seed(0)
    agent = RandomAgent(env.action_space)
    reward = 0
    done = False

    total_reward = 0
    total_guesses = 0

    for i in range(episode_count):
        ob = env.reset()
        while True:
            action = agent.act(ob, reward, done)
            ob, reward, done, info = env.step(action)

            if done:
                total_reward += reward
                total_guesses += int(info["guesses"])
                break

    # Close the env and write monitor result info to disk
    env.close()
    print(f'Total random reward {total_reward}, average guess {round(total_guesses / 100, 1)}')

 

几个可能的运行结果:

Total better random reward 100, average guess 35.9
Total random reward 15, average guess 180.6
-----
Total better random reward 100, average guess 39.2
Total random reward 20, average guess 175.9
--
Total better random reward 100, average guess 38.2
Total random reward 24, average guess 177.2
--
Total better random reward 100, average guess 38.6
Total random reward 18, average guess 180.4

 

可以看到改进后的Agent,几乎每次都猜中,平均30多次就猜中,而完全随机的Agent,猜中20次左右,每次需要180次左右。

如果我们使用二分法来设计Agent,由于事先不知道所猜数的范围,所以可以先设计算法,得到所猜数的范围,比如从100开始,每次翻倍,直到observation从偏小变成偏大。
我们的例子的取值范围为(-10000,10000),因此只要8次就可以得到范围为[-25600,25600]。然后使用二分法,最多也是8次就可以猜中数字。

本篇源码 https://github.com/guidebee/guessing_number

Open AI Gym教程(5): 各种环境Wrappers

gym.wrappers自带个多种封装后的Wrapper,对Action,Reward,Obersvation 做了处理(其中Observation Wrapper可以看成类似于图像处理时的各种滤镜)

 

其中的Monitor 可以用来观测环境的参数并保持视频到本地。

outdir = '/tmp/random-agent-results'
env = wrappers.Monitor(env, directory=outdir, force=True)

TimeLimit 可以控制最大的episode的步数。
FrameStack 可以多个Observation 组合成一个多帧的Observation,比如你的AI算法需要连续的几个帧的数据做为输入。
RecordEpisodeStatistics 可以用来记录episodes的一些统计信息
PixelObservationWrapper 可以为原先的Observation增加一些图像数据。

此外gym.vector 目录定义了矢量化环境,一般Gym环境是单个环境,如果我们需要同时使用多个环境来设计我们的AI算法,我们使用使用矢量化环境。这时envs, actions, rewards,和 observations都会是个一维数组。