“游戏”语义化问题

avatar
阿里巴巴 前端委员会智能化小组 @阿里巴巴

文/ 阿里淘系 F(x) Team - 民超

引言

界面元素语义化一直是困扰D2C以及AI界的难题,而语义化是人工智能在代码生成产品(比如D2C)中的关键环节,对人性化设计起到至关重要效果。目前,国际上常见的语义化技术大多是从纯字段本身从发,如TxtCNN、Attention、Bert等,这些方法固然效果很好,但应用于D2C产品还是有一定的局限性,这是因为D2C愿景是成为一个端到端的系统,单从纯字段出发很难对其进行语义化,比如“¥200”这个字段是很难绑定合适的语义,有可能是原价、活动价等意思。因此D2C要想解决界面元素语义化任务至少需要解决两个问题:1. 能够生成符合当前界面的元素语义;2. 降低使用者的约束,不需要使用者额外输入辅助信息。

近些年,强化学习在很多领域表现突出,如AlphaGo、机器人、自动驾驶、游戏等,优秀的表现吸引着众多学者的研究。在本文中,针对界面元素语义化问题,引入了基于游戏思想的深度强化学习(DRL,Deep Reinforcement Learning),提出一种基于深度强化学习的语义化解决方案,创新性地构造了一种适用于语义化问题的强化学习训练环境,并进行了2种不同类型的深度强化学习算法(1. 基于值函数的强化学习算法DQN 2. 结合基于值函数和基于策略梯度的Actor-Critic算法DPPO)的比较实验,实验结果证明了本文方法的有效性以及基于DPPO算法的语义化方案的优越性。

在本文中将界面元素语义化问题视为此情此景的决策问题,直接从界面图片入手,将界面图片作为基于深度强化学习的输入,深度强化学习通过不断地“试错”机制来学到最优策略(即最优语义)。本文具体工作如下:1. 为了更好的描述本文工作原理,故对其关键技术DQN算法和DPPO算法进行详细介绍;2. 为基于强化学习的语义化模型训练构造了趣味性的训练环境;3. 分析实验结果。

关于深度强化学习技术的详细数学基础知识可以阅读我的分享文档:[深度强化学习技术概述] (zhuanlan.zhihu.com/p/283438275…

本文所用技术概述

基于值函数的深度强化学习算法DQN

本文用到基于值函数的深度强化学习算法作为语义字段识别的具体实现技术之一,基于值函数的深度强化学习算法利用CNN来逼近传统强化学习的动作值函数,代表算法就是DQN算法, DQN算法框架如下: image.png 上图可以看出,基于值函数的深度强化学习算法DQN特征之一就是使用深度卷积神经网络逼近动作值函数。其中,状态S和S'为多维数据;经验池用于存储训练过程中的成败经验。

DQN中有两个相同结构的神经网络,分别称为目标网络和评估网络。目标网络中的输出值image.png表示当在状态S下选择动作a时的衰减得分,即: image.png 式中,r和S'分别为在状态S下采取动作a时的得分和对应的下一个状态;image.png为衰减因子;a'为评估网络输出的Q值向量表中在状态S'能获取到最大image.png值的动作,image.png为目标网络的权重参数。

评估网络的输出值image.png表示当在状态S时采取动作a的价值,即: image.png 式中, image.png为评估网络的权重参数。

DQN训练过程中分为三个阶段: (1)初始阶段。这时经验池D未满,在每一个时刻t中随机选择行为获取经验元组image.png,然后将每一步的经验元组存储至经验池image.png。这个阶段主要用来积攒经验,此时DQN的两个网络均不进行训练;

(2)探索阶段。这一阶段采用了image.png-贪心策略(image.png从1至0逐渐减少)获取动作a,在网络产生决策的同时,又能以一定的概率探索其他可能的最优行为,避免了陷入局部最优解的问题。这个阶段中不断更新经验池中的经验元组,并作为评估网络、目标网络的输入,得到image.pngimage.png。然后两者差值作为损失函数,以梯度下降算法更新评估网络的权重参数。为了使训练收敛,目标网络的权重参数更新方式为:每隔一段固定的迭代次数,将评估网络的权重参数复制给目标网络参数;

(3)利用阶段。这一阶段image.png降为0,即选择的动作全部来自评估网络的输出。评估网络和目标网络的更新方法和探索阶段一样。

基于值函数的深度强化学习算法DQN按上述三个阶段进行网络训练,当网络训练收敛,评估网络将逼近最优动作值函数,实现最优策略学习目的。

DPPO(Distribution Proximal Policy Optimization)分布式近端策略优化算法

本人尝试了将DQN算法应用于字段绑定任务中,发现DQN算法很难支持多张图片一起训练,因此转而寻找更高阶有效的算法,经研究发现PPO是目前非常流行的强化学习算法,OpenAI把PPO作为目前的默认算法,可想而知,PPO可能不是目前最强的,但可能是目前来说适用性最广的一种算法。除此之外,PPO更易处理复杂环境,因此想把PPO引入语义化任务中。而DPPO是PPO的分布式版本, 比如8个worker,每个worker都有独立的experience。  由于可以避免experience间的相关性,训练更快,故DPPO明显优于PPO。

PPO算法

首先介绍PPO算法,PPO算法是基于Actor-Critic(AC)架构的,是一种可以自适应 learning rate的AC算法,一句话概括PPO:解决Policy Gradient不好确定Learning rate(或者Step size)问题。 因为如果step size过大, 学出来的Policy会一直乱动, 不会收敛, 但如果Step Size太小, 对于完成训练, 我们会等到绝望。 PPO 利用New Policy和Old Policy的比例, 限制了New Policy的更新幅度, 让Policy Gradient对稍微大点的Step size不那么敏感。

PPO算法如下: image.png 总的来说 PPO 是一套 Actor-Critic 结构, Actor 想最大化 J_PPO, Critic 想最小化 L_BL。Critic 的 loss 好说, 就是减小时序差分(TD) error。而 Actor 的就是在 old Policy 上根据 Advantage (TD error) 修改 new Policy, advantage 大的时候, 修改幅度大, 让 new Policy 更可能发生。而且附加了一个 KL Penalty (惩罚项, 不懂的同学搜一下 KL divergence)来控制Learning rate, 简单来说, 如果 new Policy 和 old Policy 差太多, 那 KL divergence 也越大, 我们不希望 new Policy 比 old Policy 差太多, 如果会差太多, 就相当于用了一个大的 Learning rate, 这样是不好的, 难收敛。

DPPO算法

Google DeepMind 提出来了一套和 A3C 类似的并行 PPO 算法,即DPPO,相比PPO其不同点总结如下:

  • 用 OpenAI 提出的 Clipped Surrogate Objective
  • 使用多个线程 (workers) 平行在不同的环境中收集数据
  • workers 共享一个 Global PPO
  • workers 不会自己算 PPO 的 gradients, 不会像 A3C 那样推送 Gradients 给 Global net
  • workers 只推送自己采集的数据给 Global PPO
  • Global PPO 拿到多个 workers 一定批量的数据后进行更新 (更新时 worker 停止采集)
  • 更新完后, workers 用最新的 Policy 采集数据

实验流程

强化学习环境构造

强化学习的重要因素:智能体、环境、反馈的奖惩函数设计、step设计。本文的思路如下:将语义字段识别任务当作一个玩游戏的过程,在这个过程中算法模型根据环境反馈不断地去更新模型参数,学到一种如何最大化奖惩函数的规律。具体为:直接选用模块图片作为环境,模块内元素的边框作为智能体,算法训练时,智能体(元素边框)会从上到下、从左到右移动,就像走迷宫一样,每走一步就要做一个决策选定行为Action(这个Action就是我们想要的语义字段),只有当Action选对了智能体才能往下“走”,当智能体把所有元素都“走”了一遍代表着算法模型学到了制胜之道。环境构造如下图: image.png

强化学习是一种无监督学习,不需要人为打标,但语义字段识别任务为了方便制造出奖惩函数人为给打上标签(假若有这个模块图片的CSS代码是不需要打标的),数据集可先用LabelImg进行打标,每一个元素都有对应语义字段,打标方式如下图所示(第一张图片是模块图片,第二张是模块内元素打标信息,第三张是语义字段信息):

image.pngimage.pngimage.png

模型训练

模型训练需要导入上面的环境代码,以DQN算法为例,模型训练及测试代码如下:

# coding:utf-8

import os
import random
import numpy as np
# import tensorflow as tf
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
from collections import deque
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from keras.models import Sequential
from keras.layers import Convolution2D, Flatten, Dense,Conv2D
from gan_env_semantic import semantic_env
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import tkinter as tk
import time
os.environ['CUDA_VISIBLE_DEVICES']='0'

ENV_NAME = 'semantic_env'#'Breakout-v0'  # Environment name
#FRAME_WIDTH = 714  # Resized frame width
#FRAME_HEIGHT = 534  # Resized frame height
NUM_EPISODES = 2000000  # Number of episodes the agent plays
STATE_LENGTH = 1  # Number of most recent frames to produce the input to the network
GAMMA = 0.9  # Discount factor
EXPLORATION_STEPS = 10000  # Number of steps over which the initial value of epsilon is linearly annealed to its final value
INITIAL_EPSILON = 1.0  # Initial value of epsilon in epsilon-greedy
FINAL_EPSILON = 0.01  # Final value of epsilon in epsilon-greedy
INITIAL_REPLAY_SIZE = 4000  #4000 Number of steps to populate the replay memory before training starts
NUM_REPLAY_MEMORY = 10000  #10000 Number of replay memory the agent uses for training
BATCH_SIZE = 64  #128 Mini batch size
TARGET_UPDATE_INTERVAL = 1000  # The frequency with which the target network is updated
TRAIN_INTERVAL = 4  # The agent selects 4 actions between successive updates
LEARNING_RATE = 0.00025  # Learning rate used by RMSProp
MOMENTUM = 0.95  # Momentum used by RMSProp
MIN_GRAD = 0.01  # Constant added to the squared gradient in the denominator of the RMSProp update
SAVE_INTERVAL = 10000  #10000 The frequency with which the network is saved
NO_OP_STEPS = 30  # Maximum number of "do nothing" actions to be performed by the agent at the start of an episode
LOAD_NETWORK = True
TRAIN = False
SAVE_NETWORK_PATH = 'saved_networks/' + ENV_NAME
SAVE_SUMMARY_PATH = 'summary/' + ENV_NAME
NUM_EPISODES_AT_TEST = 300  # Number of episodes the agent plays at test time


class Agent():
    def __init__(self, num_actions, FRAME_WIDTH, FRAME_HEIGHT):
        self.num_actions = num_actions
        self.epsilon = INITIAL_EPSILON
        self.epsilon_step = (INITIAL_EPSILON - FINAL_EPSILON) / EXPLORATION_STEPS
        self.t = 0
        self.FRAME_WIDTH, self.FRAME_HEIGHT = FRAME_WIDTH, FRAME_HEIGHT

        # Parameters used for summary
        self.total_reward = 0
        self.total_q_max = 0
        self.total_loss = 0
        self.duration = 0
        self.episode = 0

        # Create replay memory
        self.replay_memory = deque()

        # Create q network
        self.s, self.q_values, q_network = self.build_network()
        q_network_weights = q_network.trainable_weights

        # Create target network
        self.st, self.target_q_values, target_network = self.build_network()
        target_network_weights = target_network.trainable_weights

        # Define target network update operation
        self.update_target_network = [target_network_weights[i].assign(q_network_weights[i]) for i in range(len(target_network_weights))]

        # Define loss and gradient update operation
        self.a, self.y, self.loss, self.grads_update = self.build_training_op(q_network_weights)


        self.sess = tf.InteractiveSession()
        self.saver = tf.train.Saver(q_network_weights)
        self.summary_placeholders, self.update_ops, self.summary_op = self.setup_summary()
        self.summary_writer = tf.summary.FileWriter(SAVE_SUMMARY_PATH, self.sess.graph)

        if not os.path.exists(SAVE_NETWORK_PATH):
            os.makedirs(SAVE_NETWORK_PATH)

        self.sess.run(tf.global_variables_initializer())

        # Load network
        if LOAD_NETWORK:
            self.load_network()

        # Initialize target network
        self.sess.run(self.update_target_network)

    def build_network(self):
        model = Sequential()

        #model.add(Convolution2D(32, (8, 8), subsample=(4, 4), activation='relu', input_shape=(FRAME_WIDTH, FRAME_HEIGHT,STATE_LENGTH)))
        #model.add(Convolution2D(64, (4, 4), subsample=(2, 2), activation='relu'))
        #model.add(Convolution2D(64, (3, 3), subsample=(1, 1), activation='relu'))
        model.add(Conv2D(32, (8, 8), strides=(4, 4), activation='relu',input_shape=(self.FRAME_WIDTH, self.FRAME_HEIGHT,1)))
        model.add(Conv2D(64, (4, 4), strides=(2, 2), activation='relu'))
        model.add(Conv2D(64, (3, 3), strides=(1, 1), activation='relu'))
        model.add(Flatten())
        model.add(Dense(512, activation='relu'))
        model.add(Dense(self.num_actions))
        s = tf.placeholder(tf.float32, [None,self.FRAME_WIDTH, self.FRAME_HEIGHT,1])
        q_values = model(s)
        return s, q_values, model

    def build_training_op(self, q_network_weights):
        a = tf.placeholder(tf.int64, [None])
        y = tf.placeholder(tf.float32, [None])

        # Convert action to one hot vector
        a_one_hot = tf.one_hot(a, self.num_actions, 1.0, 0.0)
        q_value = tf.reduce_sum(tf.multiply(self.q_values, a_one_hot), reduction_indices=1)

        # Clip the error, the loss is quadratic when the error is in (-1, 1), and linear outside of that region
        error = tf.abs(y - q_value)
        quadratic_part = tf.clip_by_value(error, 0.0, 1.0)
        linear_part = error - quadratic_part
        loss = tf.reduce_mean(0.5 * tf.square(quadratic_part) + linear_part)

        optimizer = tf.train.RMSPropOptimizer(LEARNING_RATE, momentum=MOMENTUM, epsilon=MIN_GRAD)
        grads_update = optimizer.minimize(loss, var_list=q_network_weights)

        return a, y, loss, grads_update

    def get_initial_state(self, observation):
        processed_observation = np.reshape(observation,(self.FRAME_WIDTH,self.FRAME_HEIGHT,1))
        state = processed_observation
        return state

    def get_action(self, state):
        if self.epsilon >= random.random() or self.t < INITIAL_REPLAY_SIZE:
            action = random.randrange(self.num_actions)
        else:
            action = np.argmax(self.q_values.eval(feed_dict={self.s: [np.float32(state)]}))

        # Anneal epsilon linearly over time
        if self.epsilon > FINAL_EPSILON and self.t >= INITIAL_REPLAY_SIZE:
            self.epsilon -= self.epsilon_step
        #print("epsilon:{},t:{}".format(self.epsilon,self.t))
        return action

    def run(self, state, action, reward, terminal, observation):
        #next_state = np.append(state[1:, :, :,:], observation, axis=0)
        next_state = observation

        # Clip all positive rewards at 1 and all negative rewards at -1, leaving 0 rewards unchanged
        reward = np.clip(reward, -1, 1)

        # Store transition in replay memory
        self.replay_memory.append((state, action, reward, next_state, terminal))
        if len(self.replay_memory) > NUM_REPLAY_MEMORY:
            self.replay_memory.popleft()

        if self.t >= INITIAL_REPLAY_SIZE:
            # Train network
            if self.t % TRAIN_INTERVAL == 0:
                self.train_network()

            # Update target network
            if self.t % TARGET_UPDATE_INTERVAL == 0:
                self.sess.run(self.update_target_network)

            # Save network
            if self.t % SAVE_INTERVAL == 0:
                save_path = self.saver.save(self.sess, SAVE_NETWORK_PATH + '/' + ENV_NAME, global_step=self.t)
                print('Successfully saved: ' + save_path)

        self.total_reward += reward
        self.total_q_max += np.max(self.q_values.eval(feed_dict={self.s: [np.float32(state)]}))
        self.duration += 1

        if terminal or self.duration % 100 == 0:
            # Write summary
            if self.t >= INITIAL_REPLAY_SIZE:
                stats = [self.total_reward, self.total_q_max / float(self.duration),
                        self.duration, self.total_loss / (float(self.duration) / float(TRAIN_INTERVAL))]
                for i in range(len(stats)):
                    self.sess.run(self.update_ops[i], feed_dict={
                        self.summary_placeholders[i]: float(stats[i])
                    })
                summary_str = self.sess.run(self.summary_op)
                self.summary_writer.add_summary(summary_str, self.episode + 1)

            if terminal:
                # Debug
                if self.t < INITIAL_REPLAY_SIZE:
                    mode = 'random'
                elif INITIAL_REPLAY_SIZE <= self.t < INITIAL_REPLAY_SIZE + EXPLORATION_STEPS:
                    mode = 'explore'
                else:
                    mode = 'exploit'

                print('EPISODE: {0:6d} / TIMESTEP: {1:8d} / DURATION: {2:5d} / EPSILON: {3:.5f} / TOTAL_REWARD: {4:3.0f} / AVG_MAX_Q: {5:2.4f} / AVG_LOSS: {6:.5f} / MODE: {7}'.format(
                    self.episode + 1, self.t, self.duration, self.epsilon,
                    self.total_reward, self.total_q_max / float(self.duration),
                    self.total_loss / (float(self.duration) / float(TRAIN_INTERVAL)), mode))

                self.total_reward = 0
                self.total_q_max = 0
                self.total_loss = 0
                self.duration = 0
                self.episode += 1

        self.t += 1

        return next_state

    def train_network(self):
        state_batch = []
        action_batch = []
        reward_batch = []
        next_state_batch = []
        terminal_batch = []
        y_batch = []

        # Sample random minibatch of transition from replay memory
        minibatch = random.sample(self.replay_memory, BATCH_SIZE)
        for data in minibatch:
            state_batch.append(data[0])
            action_batch.append(data[1])
            reward_batch.append(data[2])
            next_state_batch.append(data[3])
            terminal_batch.append(data[4])

        # Convert True to 1, False to 0
        terminal_batch = np.array(terminal_batch) + 0

        next_action_batch = np.argmax(self.q_values.eval(feed_dict={self.s: next_state_batch}), axis=1)
        target_q_values_batch = self.target_q_values.eval(feed_dict={self.st: next_state_batch})
        for i in range(len(minibatch)):
            y_batch.append(reward_batch[i] + (1 - terminal_batch[i]) * GAMMA * target_q_values_batch[i][next_action_batch[i]])

        loss, _ = self.sess.run([self.loss, self.grads_update], feed_dict={
            self.s: np.float32(np.array(state_batch)),
            self.a: action_batch,
            self.y: y_batch
        })

        self.total_loss += loss

    def setup_summary(self):
        episode_total_reward = tf.Variable(0.)
        tf.summary.scalar(ENV_NAME + '/Total Reward/Episode', episode_total_reward)
        episode_avg_max_q = tf.Variable(0.)
        tf.summary.scalar(ENV_NAME + '/Average Max Q/Episode', episode_avg_max_q)
        episode_duration = tf.Variable(0.)
        tf.summary.scalar(ENV_NAME + '/Duration/Episode', episode_duration)
        episode_avg_loss = tf.Variable(0.)
        tf.summary.scalar(ENV_NAME + '/Average Loss/Episode', episode_avg_loss)
        summary_vars = [episode_total_reward, episode_avg_max_q, episode_duration, episode_avg_loss]
        summary_placeholders = [tf.placeholder(tf.float32) for _ in range(len(summary_vars))]
        update_ops = [summary_vars[i].assign(summary_placeholders[i]) for i in range(len(summary_vars))]
        summary_op = tf.summary.merge_all()
        return summary_placeholders, update_ops, summary_op

    def load_network(self):
        checkpoint = tf.train.get_checkpoint_state(SAVE_NETWORK_PATH)
        if checkpoint and checkpoint.model_checkpoint_path:
            self.saver.restore(self.sess, checkpoint.model_checkpoint_path)
            print('Successfully loaded: ' + checkpoint.model_checkpoint_path)
        else:
            print('Training new network...')

    def get_action_at_test(self, state):
        """
        if random.random() <= 0.05:
            action = random.randrange(self.num_actions)
        else:
            action = np.argmax(self.q_values.eval(feed_dict={self.s: [np.float32(state)]}))
        """
        action = np.argmax(self.q_values.eval(feed_dict={self.s: [np.float32(state)]}))
        self.t += 1
        return action

def main():
    root = tk.Tk()
    root.title("matplotlib in TK")
    f = Figure(figsize=(6, 6), dpi=100)
    canvas = FigureCanvasTkAgg(f, master=root)
    canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)

    env = semantic_env()
    # FRAME_WIDTH, FRAME_HEIGHT= env.img_width,env.img_height
    agent = Agent(num_actions=env.len_actions, FRAME_WIDTH = env.img_width, FRAME_HEIGHT = env.img_height)

    if TRAIN:  # Train mode
        for _ in range(NUM_EPISODES):
            terminal = False

            observation, curr_rec_class = env.env_reset()
            state = agent.get_initial_state(observation)

            while not terminal:

                f.clf()
                a = f.add_subplot(111)
                a.imshow(observation[:, :], interpolation='nearest', aspect='auto', cmap='gray')
                a.axis('off')
                canvas.draw()
                root.update()

                action = agent.get_action(state)
                observation,reward, terminal, curr_rec_class = env.step(action,curr_rec_class)
                print(action,env.action_space[action],reward,terminal)
                if env.current_rec_num == 0:
                    print("hahahhahahhaaha I am Winner!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
                processed_observation = np.reshape(observation, (env.img_width, env.img_height, 1))

                state = agent.run(state, action, reward, terminal, processed_observation)
    else:  # Test mode
        for _ in range(NUM_EPISODES_AT_TEST):
            terminal = False
            observation, curr_rec_class = env.env_reset()
            state = agent.get_initial_state(observation)

            while not terminal:
                f.clf()
                a = f.add_subplot(111)
                a.imshow(observation[:, :], interpolation='nearest', aspect='auto', cmap='gray')
                a.axis('off')
                canvas.draw()
                root.update()

                action = agent.get_action_at_test(state)
                observation,reward, terminal, curr_rec_class = env.step(action,curr_rec_class)

                print('This semantic word is ->   ' + env.action_space[action]+'\n')
                if env.current_rec_num == 0:
                    pass
                state = np.reshape(observation, (env.img_width, env.img_height, 1))


if __name__ == '__main__':
    main()

模型评估及结果展示

模型评估

强化学习模型的评估方式不同于分类检测问题,没有像准确率这样的衡量指标,它的衡量指标就是得分一直上升不再下降,就是游戏不再重来。本文实验结果确实是得分一直上升,没有重来。总体得分变化如下: image.png

模型效果展示

DQN算法训练结果展示如下: semantic_result.GIF

DPPO算法训练结果展示如下,可以看出这个算法可以支持多张图片训练。 2.gif

实验分析

目前该模型虽能成功的进行语义化任务,但该模型还有很多优化点和不足之处:

  1. 由于该算法是把像素图片作为训练数据集,故训练出的模型对元素样式敏感,识别效果好,但对于没有样式的纯文字效果会稍逊一筹,关于这一点可以考虑将纯文字的输入到文本分类模型中进行分类。
  2. 该算法应用场景广,是一套模型训练框架,可以应用于多个其它任务中。