Explicação detalhada do código do algoritmo DDPG

Otimização de descarregamento de computação para computação de borda móvel assistida por UAV: ​​uma abordagem de gradiente de política determinística profunda

Documentos de referência:
[1] Wang Y , Fang W , Ding Y , et al. Otimização de descarregamento de computação para computação de borda móvel assistida por UAV: ​​uma abordagem de gradiente de política determinística profunda [J]. Redes sem fio, 2021: 1-16.doi: https://doi.org/10.1007/s11276-021-02632-zCode
:
fangvv/UAV-DDPG
Combinando papers e códigos open source para explicar o algoritmo DDPG em detalhes, aqui está o código para rodar bem (o código aqui também é alterado de acordo com a Internet, O algoritmo DDPG já está corrigido e a inovação só pode ser feita em termos de modelagem. A classe de ambiente precisa ser escrita por você e o algoritmo DDPG pode ser aplicado diretamente). Use tensorboard para exportar o tensor gráfico de fluxo Esta é uma ferramenta de visualização muito boa que ajuda Para esclarecer o código, o diagrama tensorboard do código é o seguinte, e o seguinte será explicado em detalhes com base neste diagrama. (Requer conhecimento teórico de aprendizado por reforço e um certo entendimento de tensorflow e python. O blog anterior tem uma rota gratuita de aprendizado do sistema de aprendizado por reforço. Será mais amigável ler este blog depois de aprender)
insira a descrição da imagem aqui


contribuir

Considerando o estado do canal variável no tempo no sistema MEC assistido por UAV com intervalo de tempo, otimize em conjunto o agendamento do usuário, o movimento do UAV e a alocação de recursos, formule o problema de descarga de computação não convexa como um problema de processo de decisão de Markov (MDP) e minimize o atraso de tempo inicial.
Considerando o modelo MDP, a complexidade do estado do sistema é muito alta, e a tomada de decisão de offloading computacional precisa suportar um espaço de ação contínuo. O algoritmo DDPG é usado para resolver esse problema, a rede Actor é usada para fazer ações e a rede Crítica é usada para aproximar a função de valor da ação Q para pontuar as ações e a melhor estratégia ótima.

estrutura DDPG

insira a descrição da imagem aqui

Código detalhado

Defina a classe DDPG, inicialize-a e a Session é uma instrução para o Tensorflow controlar e executar o arquivo de saída. Execute session.run() para obter o resultado do cálculo que deseja saber ou a parte que deseja calcular. Você irá use session.run later () para inicializar a variável. placeholder é um espaço reservado no Tensorflow. Ele armazena variáveis ​​temporariamente. Pode ser entendido como um shell vazio e o valor é passado para cálculo. Se não for passado, o shell vazio não realiza nenhum cálculo.

def __init__(self, a_dim, s_dim, a_bound):
        self.memory = np.zeros((MEMORY_CAPACITY, s_dim * 2 + a_dim + 1), dtype=np.float32)  # memory里存放当前和下一个state,动作和奖励
        self.pointer = 0
        self.sess = tf.Session()

        self.a_dim, self.s_dim, self.a_bound = a_dim, s_dim, a_bound,
        self.S = tf.placeholder(tf.float32, [None, s_dim], 's')  # 输入
        self.S_ = tf.placeholder(tf.float32, [None, s_dim], 's_')
        self.R = tf.placeholder(tf.float32, [None, 1], 'r')

tf.variable_scope Entendimento pessoal, principalmente para compartilhar variáveis, nomes diferentes podem compartilhar a mesma variável, DDPG tem um total de quatro redes neurais, a estrutura das duas redes neurais no Actor é a mesma e as duas redes neurais no Critic A estrutura da rede neural é o mesmo, mas os parâmetros são diferentes. Depois de escrever a estrutura de uma rede neural, essas variáveis ​​podem ser compartilhadas nomeando-as de maneira diferente. Como pode ser visto no código a seguir, o Actor tem duas redes, uma é a rede principal eval e a outra é a rede de destino, a rede de avaliação gera diretamente a ação a e a rede de destino gera a ação a_, ambas as quais são construídos usando a função _build_a e são nomeados de forma diferente Implemente o compartilhamento de variável. O mesmo vale para a rede Crítica, que são as redes eval e alvo, respectivamente, e a saída são dois valores Q, q, q_. No tensorboard, você pode ver que há duas redes sob o ator e o crítico. Vamos dar uma olhada em como construir uma rede de atores e uma rede crítica. trainable=True ou False é usado principalmente para especificar se as variáveis ​​na rede neural devem ser adicionadas ao atlas tensorboard.

        with tf.variable_scope('Actor'):
            self.a = self._build_a(self.S, scope='eval', trainable=True)
            a_ = self._build_a(self.S_, scope='target', trainable=False)
        with tf.variable_scope('Critic'):
            # assign self.a = a in memory when calculating q for td_error,
            # otherwise the self.a is from Actor when updating Actor
            q = self._build_c(self.S, self.a, scope='eval', trainable=True)
            q_ = self._build_c(self.S_, a_, scope='target', trainable=False)

insira a descrição da imagem aqui

Ator

Depois que a rede de atores insere principalmente o estado s, ela imprime diretamente a ação a. tf.layers.dense( input, units=k ) gerará automaticamente um núcleo de matriz de peso e compensará o viés do item internamente. As dimensões específicas de cada variável são as seguintes: Para uma entrada de tensor bidimensional com um tamanho de [m, n ], tf.layers.dense() gerará: um núcleo de matriz de peso de tamanho [n, k] e um viés de item de deslocamento de tamanho [m, k]. O processo de cálculo interno é y = entrada * kernel + viés, e a dimensão do valor de saída y é [m, k].
Usando a função de camada totalmente conectada encapsulada por tensorflow, o estado de entrada, a primeira camada de neurônios 400 (chamada l1), a segunda camada 300 (chamada l2), a terceira camada 10 (chamada l3), a camada de saída 4 (a_dim = 4 chamado a). Duas redes com a mesma estrutura do ator são construídas, e o tensorboard é mostrado na figura.

def _build_a(self, s, scope, trainable):
        with tf.variable_scope(scope):
            net = tf.layers.dense(s, 400, activation=tf.nn.relu6, name='l1', trainable=trainable)
            net = tf.layers.dense(net, 300, activation=tf.nn.relu6, name='l2', trainable=trainable)
            net = tf.layers.dense(net, 10, activation=tf.nn.relu, name='l3', trainable=trainable)
            a = tf.layers.dense(net, self.a_dim, activation=tf.nn.tanh, name='a', trainable=trainable)
            return tf.multiply(a, self.a_bound[1], name='scaled_a')

insira a descrição da imagem aqui

Crítico

A seguir está a rede Crítica, que pontua a ação a e gera o valor Q. Isso é um pouco diferente do método acima de construir uma rede neural, porque a função Q precisa inserir o estado s e a ação a, então s corresponde a um peso, a corresponde a um peso e há um viés como um todo, e, em seguida, passar por uma função de ativação é equivalente a Uma camada de rede neural, w1_s, w1_a e b1 são os parâmetros da rede neural, que precisam ser salvos e atualizados, para que sejam definidos como treináveis, para que a entrada para a primeira camada de neurônios é 400 e a segunda camada é 300 (chamada l2 ), a terceira camada é 10 (chamada l3) e a saída é a pontuação da ação, escalar. O princípio da rede neural, através da função de ativação (função não linear) após a soma linear. As duas redes de Critic com a mesma estrutura são construídas, e o tensorboard é mostrado na figura.

def _build_c(self, s, a, scope, trainable):
        with tf.variable_scope(scope):
            n_l1 = 400
            w1_s = tf.get_variable('w1_s', [self.s_dim, n_l1], trainable=trainable)
            w1_a = tf.get_variable('w1_a', [self.a_dim, n_l1], trainable=trainable)
            b1 = tf.get_variable('b1', [1, n_l1], trainable=trainable)
            net = tf.nn.relu6(tf.matmul(s, w1_s) + tf.matmul(a, w1_a) + b1)
            net = tf.layers.dense(net, 300, activation=tf.nn.relu6, name='l2', trainable=trainable)
            net = tf.layers.dense(net, 10, activation=tf.nn.relu, name='l3', trainable=trainable)
            return tf.layers.dense(net, 1, trainable=trainable)  # Q(s,a)

insira a descrição da imagem aqui
insira a descrição da imagem aqui

insira a descrição da imagem aqui
Até agora, as quatro estruturas de rede neural foram construídas.

Os parâmetros importantes da rede neural são o peso e o viés de cada camada. Não construímos a rede neural de uma forma muito primitiva, mas chamamos diretamente tf.layers.dense para construí-la. Os parâmetros específicos de cada camada não são claros , mas tensorflow fornece uma O conveniente tf.get_collection extrai todas as variáveis ​​de uma coleção. É uma lista. Existem quatro redes no total. Desta forma, todos os parâmetros das quatro redes podem ser extraídos para atualização.

# networks parameters
        self.ae_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Actor/eval')
        self.at_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Actor/target')
        self.ce_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Critic/eval')
        self.ct_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Critic/target')

Piscina de replays de experiência

Como a entrada da rede neural geralmente não é inserida uma a uma, mas na forma de pequenos lotes, é necessário coletar uma certa quantidade de experiência e colocá-la no pool de reprodução de experiência. Cada pequeno lote é retirado para treinar o neural rede, e o treinamento atinge um certo número de vezes. Pare de treinar. Chame s, a, r, s_ um pedaço de experiência e armazene-o no pool de experiência até que a capacidade máxima seja atingida.

    def store_transition(self, s, a, r, s_):
        transition = np.hstack((s, a, [r], s_))
        # transition = np.hstack((s, [a], [r], s_))
        index = self.pointer % MEMORY_CAPACITY  # replace the old memory with new memory
        self.memory[index, :] = transition
        self.pointer += 1

Atualização de Parâmetros de Rede Neural

As duas redes principais Actor_eval e Critic_eval em Actor e Critic são atualizadas por meio de TError e gradiente de política, respectivamente.
insira a descrição da imagem aqui
Calcule TError para atualizar os parâmetros de rede de Critic_eval, e os parâmetros de rede de Actor_eval são derivados dos parâmetros de actor_eval por meio da função Q. Aqui, Q está realmente derivando a e, em seguida, derivando mu. {\theta^Q}
é o ce_params aqui, e {\theta^\mu} é o ae_params. q_target corresponde a y_i.

        q_target = self.R + GAMMA * q_
        # in the feed_dic for the td_error, the self.a should change to actions in memory
        td_error = tf.losses.mean_squared_error(labels=q_target, predictions=q)
        self.ctrain = tf.train.AdamOptimizer(LR_C).minimize(td_error, var_list=self.ce_params)

        a_loss = - tf.reduce_mean(q)  # maximize the q
        self.atrain = tf.train.AdamOptimizer(LR_A).minimize(a_loss, var_list=self.ae_params)

Há um total de quatro redes neurais. Os parâmetros da rede principal são atualizados acima, e as duas redes de destino também precisam ser atualizadas. O método de atualização suave (média deslizante) é adotado e cada atualização é um pouco bit. Por que usar duas redes neurais em Ator e Crítico, respectivamente? Uma rede neural é principalmente para evitar o problema de superestimação, portanto, os parâmetros precisam ser definidos de maneira diferente. O método de atualização difícil é atrasar os parâmetros da rede principal e copiá-los para a rede de destino, enquanto a atualização suave é equivalente a atualizar um pouco a cada vez. Sua fórmula e código correspondente são os seguintes.
insira a descrição da imagem aqui

        # target net replacement
        self.soft_replace = [tf.assign(t, (1 - TAU) * t + TAU * e)
                             for t, e in zip(self.at_params + self.ct_params, self.ae_params + self.ce_params)]

A fórmula aqui pode atualizar os parâmetros das duas redes de destino de uma só vez? Por que aqui self.at_params + self.ct_params aqui é +? Para ser honesto, não entendo muito bem, então tanto at_params quanto ct_params podem ser atualizados? Alguém que conheça pode explicar detalhadamente? Mas depois de procurar por isso, parece que esse é o jeito certo de escrever. Este também é um código irritante para usar diretamente.
insira a descrição da imagem aqui

salvar experiência

Neste ponto, todo o framework do DDPG está basicamente construído, e o resto é para armazenar os dados no pool de experiência. Depois que o armazenamento estiver cheio, você pode começar a treinar a rede neural construída. Acessar a experiência (s, a, r, s_), selecionar aleatoriamente um estado s, neste momento é necessário enviar uma ação através da rede neural, então a função choose_action é definida, aqui a rede neural é chamada para gerar a ação a, esta ação é muitas vezes representada pelo ruído Explore, e então o ambiente recompensa r de acordo com a ação a e atualiza o próximo estado s_, de forma que uma experiência seja obtida. Ao salvar, ele julgará se o armazenamento está cheio ou se possui certos requisitos para ações de saída etc., consulte o código abaixo. Neste momento, a rede neural não pode ser treinada.

    def choose_action(self, s):
        temp = self.sess.run(self.a, {
    
    self.S: s[np.newaxis, :]})
        return temp[0]
# Add exploration noise
        a = ddpg.choose_action(s_normal.state_normal(s))
        a = np.clip(np.random.normal(a, var), *a_bound)  # 高斯噪声add randomness to action selection for exploration
        s_, r, is_terminal, step_redo, offloading_ratio_change, reset_dist = env.step(a)
        if step_redo:
            continue
        if reset_dist:
            a[2] = -1
        if offloading_ratio_change:
            a[3] = -1
        ddpg.store_transition(s_normal.state_normal(s), a, r, s_normal.state_normal(s_))  # 训练奖励缩小10倍

trem

Depois de julgar que o pool de experiência está cheio, comece a treinar a rede neural e atualizar os parâmetros. Aqui, você pode chamar diretamente a função de aprendizado para iniciar o processo de treinamento e atualizar os parâmetros.

        if ddpg.pointer > MEMORY_CAPACITY:
            # var = max([var * 0.9997, VAR_MIN])  # decay the action randomness
            ddpg.learn()

Depois de construir o gráfico de fluxo, use sess.run para executar, primeiro execute uma atualização suave (atualização do parâmetro de rede de destino), retire aleatoriamente a experiência BATCH_SIZE do pool de experiência e use sess.run para atualizar os principais parâmetros de rede (atrain, ctrain) .

    def learn(self):
        self.sess.run(self.soft_replace)

        indices = np.random.choice(MEMORY_CAPACITY, size=BATCH_SIZE)
        bt = self.memory[indices, :]
        bs = bt[:, :self.s_dim]
        ba = bt[:, self.s_dim: self.s_dim + self.a_dim]
        br = bt[:, -self.s_dim - 1: -self.s_dim]
        bs_ = bt[:, -self.s_dim:]

        self.sess.run(self.atrain, {
    
    self.S: bs})
        self.sess.run(self.ctrain, {
    
    self.S: bs, self.a: ba, self.R: br, self.S_: bs_})
        if ddpg.pointer > MEMORY_CAPACITY:
            # var = max([var * 0.9997, VAR_MIN])  # decay the action randomness
            ddpg.learn()
        s = s_
        ep_reward += r

O código da classe DDPG é explicado aqui, e o código da classe DDPG é divulgado aqui.

###############################  DDPG  ####################################
class DDPG(object):
    def __init__(self, a_dim, s_dim, a_bound):
        self.memory = np.zeros((MEMORY_CAPACITY, s_dim * 2 + a_dim + 1), dtype=np.float32)  # memory里存放当前和下一个state,动作和奖励
        self.pointer = 0
        self.sess = tf.Session()

        self.a_dim, self.s_dim, self.a_bound = a_dim, s_dim, a_bound,
        self.S = tf.placeholder(tf.float32, [None, s_dim], 's')  # 输入
        self.S_ = tf.placeholder(tf.float32, [None, s_dim], 's_')
        self.R = tf.placeholder(tf.float32, [None, 1], 'r')

        with tf.variable_scope('Actor'):
            self.a = self._build_a(self.S, scope='eval', trainable=True)
            a_ = self._build_a(self.S_, scope='target', trainable=False)
        with tf.variable_scope('Critic'):
            # assign self.a = a in memory when calculating q for td_error,
            # otherwise the self.a is from Actor when updating Actor
            q = self._build_c(self.S, self.a, scope='eval', trainable=True)
            q_ = self._build_c(self.S_, a_, scope='target', trainable=False)

        self.ae_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Actor/eval')
        self.at_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Actor/target')
        self.ce_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Critic/eval')
        self.ct_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Critic/target')

        # target net replacement
        self.soft_replace = [tf.assign(t, (1 - TAU) * t + TAU * e)
                             for t, e in zip(self.at_params + self.ct_params, self.ae_params + self.ce_params)]

        q_target = self.R + GAMMA * q_
        # in the feed_dic for the td_error, the self.a should change to actions in memory
        td_error = tf.losses.mean_squared_error(labels=q_target, predictions=q)
        self.ctrain = tf.train.AdamOptimizer(LR_C).minimize(td_error, var_list=self.ce_params)

        a_loss = - tf.reduce_mean(q)  # maximize the q
        self.atrain = tf.train.AdamOptimizer(LR_A).minimize(a_loss, var_list=self.ae_params)

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

        if OUTPUT_GRAPH:
            tf.summary.FileWriter("logs/", self.sess.graph)

    def choose_action(self, s):
        temp = self.sess.run(self.a, {
    
    self.S: s[np.newaxis, :]})
        return temp[0]

    def learn(self):
        self.sess.run(self.soft_replace)

        indices = np.random.choice(MEMORY_CAPACITY, size=BATCH_SIZE)
        bt = self.memory[indices, :]
        bs = bt[:, :self.s_dim]
        ba = bt[:, self.s_dim: self.s_dim + self.a_dim]
        br = bt[:, -self.s_dim - 1: -self.s_dim]
        bs_ = bt[:, -self.s_dim:]

        self.sess.run(self.atrain, {
    
    self.S: bs})
        self.sess.run(self.ctrain, {
    
    self.S: bs, self.a: ba, self.R: br, self.S_: bs_})

    def store_transition(self, s, a, r, s_):
        transition = np.hstack((s, a, [r], s_))
        # transition = np.hstack((s, [a], [r], s_))
        index = self.pointer % MEMORY_CAPACITY  # replace the old memory with new memory
        self.memory[index, :] = transition
        self.pointer += 1

    def _build_a(self, s, scope, trainable):
        with tf.variable_scope(scope):
            net = tf.layers.dense(s, 400, activation=tf.nn.relu6, name='l1', trainable=trainable)
            net = tf.layers.dense(net, 300, activation=tf.nn.relu6, name='l2', trainable=trainable)
            net = tf.layers.dense(net, 10, activation=tf.nn.relu, name='l3', trainable=trainable)
            a = tf.layers.dense(net, self.a_dim, activation=tf.nn.tanh, name='a', trainable=trainable)
            return tf.multiply(a, self.a_bound[1], name='scaled_a')

    def _build_c(self, s, a, scope, trainable):
        with tf.variable_scope(scope):
            n_l1 = 400
            w1_s = tf.get_variable('w1_s', [self.s_dim, n_l1], trainable=trainable)
            w1_a = tf.get_variable('w1_a', [self.a_dim, n_l1], trainable=trainable)
            b1 = tf.get_variable('b1', [1, n_l1], trainable=trainable)
            net = tf.nn.relu6(tf.matmul(s, w1_s) + tf.matmul(a, w1_a) + b1)
            net = tf.layers.dense(net, 300, activation=tf.nn.relu6, name='l2', trainable=trainable)
            net = tf.layers.dense(net, 10, activation=tf.nn.relu, name='l3', trainable=trainable)
            return tf.layers.dense(net, 1, trainable=trainable)  # Q(s,a)

Exceto pelas poucas sentenças a seguir, inicialize todas as variáveis ​​e gere o gráfico do tensorboard.

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

        if OUTPUT_GRAPH:
            tf.summary.FileWriter("logs/", self.sess.graph)

Código de treinamento:

###############################  training  ####################################
np.random.seed(1)
tf.set_random_seed(1)

env = UAVEnv()
MAX_EP_STEPS = env.slot_num
s_dim = env.state_dim
a_dim = env.action_dim
a_bound = env.action_bound  # [-1,1]

ddpg = DDPG(a_dim, s_dim, a_bound)

# var = 1  # control exploration
var = 0.01  # control exploration
t1 = time.time()
ep_reward_list = []
s_normal = StateNormalization()

for i in range(MAX_EPISODES):
    s = env.reset()
    ep_reward = 0

    j = 0
    while j < MAX_EP_STEPS:
        # Add exploration noise
        a = ddpg.choose_action(s_normal.state_normal(s))
        a = np.clip(np.random.normal(a, var), *a_bound)  # 高斯噪声add randomness to action selection for exploration
        s_, r, is_terminal, step_redo, offloading_ratio_change, reset_dist = env.step(a)
        if step_redo:
            continue
        if reset_dist:
            a[2] = -1
        if offloading_ratio_change:
            a[3] = -1
        ddpg.store_transition(s_normal.state_normal(s), a, r, s_normal.state_normal(s_))  # 训练奖励缩小10倍

        if ddpg.pointer > MEMORY_CAPACITY:
            # var = max([var * 0.9997, VAR_MIN])  # decay the action randomness
            ddpg.learn()
        s = s_
        ep_reward += r
        if j == MAX_EP_STEPS - 1 or is_terminal:
            print('Episode:', i, ' Steps: %2d' % j, ' Reward: %7.2f' % ep_reward, 'Explore: %.3f' % var)
            ep_reward_list = np.append(ep_reward_list, ep_reward)
            # file_name = 'output_ddpg_' + str(self.bandwidth_nums) + 'MHz.txt'
            file_name = 'output.txt'
            with open(file_name, 'a') as file_obj:
                file_obj.write("\n======== This episode is done ========")  # 本episode结束
            break
        j = j + 1

    # # Evaluate episode
    # if (i + 1) % 50 == 0:
    #     eval_policy(ddpg, env)

print('Running time: ', time.time() - t1)
plt.plot(ep_reward_list)
plt.xlabel("Episode")
plt.ylabel("Reward")
plt.show()

おすすめ

転載: blog.csdn.net/weixin_43835470/article/details/120881273