一、策略梯度算法回顾

策略梯度(Policy Gradient)算法目标函数的梯度更新公式为:
▽ R ˉ θ = 1 N ∑ n = 1 N ∑ t = 1 T n ( ∑ t ′ = t T n γ t ′ − t r t ′ n − b ) ▽ l o g p θ ( a t n ∣ s t n ) (1) \bigtriangledown \bar{R}_{\theta } = \frac{1}{N}\sum_{n=1}^{N}\sum_{t=1}^{T_{n}}(\sum_{{t}'=t}^{T_{n}}\gamma ^{{t}'-t}r_{{t}'}^{n}-b)\bigtriangledown logp_{\theta }(a_{t}^{n}|s_{t}^{n})\tag{1} ▽Rˉθ​=N1​n=1∑N​t=1∑Tn​​(t′=t∑Tn​​γt′−trt′n​−b)▽logpθ​(atn​∣stn​)(1)
公式(1)各个参数详解: N N N表示采样的样本个数,因为我们要数值逼近奖励的期望; γ \gamma γ 表示折扣因子,指actor对未来奖励的重视程度,越接近1越重视未来各个时间步的奖励,越接近0越不重视; r t ′ n r_{{t}'}^{n} rt′n​ 表示第 n n n条采样样本第 t ′ {t}' t′ 时刻的奖励; b b b 表示baseline,加了baseline之后,奖励就会有正有负,负奖励对应的动作的概率就会在优化之后下降,而正的奖励对应的动作的概率就会增加,也就是baseline起到了不能将所有采样得到的动作对应的概率都增大,这是因为采样得到的动作未必是好的动作。 p θ ( a t n ∣ s t n ) p_{\theta }(a_{t}^{n}|s_{t}^{n}) pθ​(atn​∣stn​) 表示给定状态 s t n s_{t}^{n} stn​时采取动作 a t n a_{t}^{n} atn​ 的概率。

现在我们取
G t n = ∑ t ′ = t T n γ t ′ − t r t ′ n (2) G_{t}^{n} = \sum_{{t}'=t}^{T_{n}}\gamma ^{{t}'-t}r_{{t}'}^{n}\tag{2} Gtn​=t′=t∑Tn​​γt′−trt′n​(2)
我们知道无论是环境还是actor本身都具有随机性(比如我们生活的现实世界里面,环境的变化是不可测的,也就是随机性很大),因此如果不采样足够多的数据,我们很难较为准确的逼近奖励的期望,但是数据太多,就会给计算资源带来负担。因次 G t n G_{t}^{n} Gtn​是一个非常不稳定的随机变量,也就方差很大,这样训练的模型的效果也会很差。只有足够很多的episode样本我们才可以更加准确的来数值逼近奖励的期望。如果还是不明白,可以参考我的策略梯度详解这篇博客。

上面我们引入了 G t n G_{t}^{n} Gtn​十分不稳定这个问题,那么我们该如何解决这个问题呢?
问题:我们是否可以使用一个神经网络来估计这个 G t n G_{t}^{n} Gtn​这个值?
答:可以,我们使用强化学习的值函数方法。

二、QLearning算法回顾

下面先来介绍两个值函数以及他们之间的关系。

  1. 状态价值函数 V π ( s ) V^{\pi}(s) Vπ(s)
    该函数的意义是:actor和环境互动,当访问到状态 s s s 时,直到互动结束,所积累的奖励的期望。
    举一个sutton强化学习书上的例子。
    例子1:
    假设我们采样得到8个eposides,分别如下:
    1、 s a , r = 0 , s b , r = 0 , E n d s_{a},r=0,s_{b},r=0,End sa​,r=0,sb​,r=0,End
    2、 s b , r = 1 , E n d s_{b},r=1,End sb​,r=1,End
    3、 s b , r = 1 , E n d s_{b},r=1,End sb​,r=1,End
    4、 s b , r = 1 , E n d s_{b},r=1,End sb​,r=1,End
    5、 s b , r = 1 , E n d s_{b},r=1,End sb​,r=1,End
    6、 s b , r = 1 , E n d s_{b},r=1,End sb​,r=1,End
    7、 s b , r = 1 , E n d s_{b},r=1,End sb​,r=1,End
    8、 s b , r = 0 , E n d s_{b},r=0,End sb​,r=0,End
    这里给出了8个采样得到的eposide,并且忽略所采取的动作,这里是采样得到8个eposide来逼近 V π ( s ) V^{\pi}(s) Vπ(s),选择采样来逼近 V π ( s ) V^{\pi}(s) Vπ(s),是因为actor所处的环境和actor本身都具有随机性,由上面的公式(1)可以看到,如果所有的eposides有无穷多个,那么计算机根本无法实现计算 V π ( s ) V^{\pi}(s) Vπ(s)
    根据上面的8个eposides,可以计算得到 V π ( s b ) V^{\pi}(s_{b}) Vπ(sb​):
    V π ( s b ) = 1 + 1 + 1 + 1 + 1 + 1 8 = 3 4 V^{\pi}(s_{b})=\frac{1+1+1+1+1+1}{8}=\frac{3}{4} Vπ(sb​)=81+1+1+1+1+1​=43​
    因为是要遇到状态 s b s_{b} sb​ 才开始计算,第一条样本和最后一条的样本在遇到状态 s b s_{b} sb​时,得到的奖励是0,因此分子上面只有6个1,而分母是8.,这里的例子只是使用8条采样的样本来数值逼近的,如果条件允许,还可以采样更多的样本来更加精确的数值逼近 V π ( s b ) V^{\pi}(s_{b}) Vπ(sb​).
  2. 状态动作价值函数 Q π ( s , a ) Q^{\pi}(s,a) Qπ(s,a)
    这个函数的意义就是actor在和环境互动过程中遇到状态 s s s,确定采取动作 a a a之后直到互动结束所累积的奖励的期望,这个函数值和上面的计算类似,下面修改一下上面那个例子,来举例说明如何计算
    例子2:
    假设我们采样得到8个eposides,分别如下:
    1、 s a , a 1 , r = 0 , s b , a 2 , r = 0 , E n d s_{a}, a_{1}, r=0,s_{b}, a_{2}, r=0,End sa​,a1​,r=0,sb​,a2​,r=0,End
    2、 s b , a 2 , r = 1 , E n d s_{b}, a_{2}, r=1,End sb​,a2​,r=1,End
    3、 s b , a 2 , r = 1 , E n d s_{b}, a_{2}, r=1,End sb​,a2​,r=1,End
    4、 s b , a 2 , r = 1 , E n d s_{b}, a_{2}, r=1,End sb​,a2​,r=1,End
    5、 s b , a 2 , r = 1 , E n d s_{b}, a_{2}, r=1,End sb​,a2​,r=1,End
    6、 s b , a 2 , r = 1 , E n d s_{b}, a_{2}, r=1,End sb​,a2​,r=1,End
    7、 s b , a 2 , r = 1 , E n d s_{b}, a_{2}, r=1,End sb​,a2​,r=1,End
    8、 s b , a 2 , r = 0 , E n d s_{b}, a_{2}, r=0,End sb​,a2​,r=0,End
    我们现在来计算 Q π ( s b , a 2 ) Q^{\pi}(s_{b},a_{2}) Qπ(sb​,a2​),因为第一条样本和第二条样本在遇到状态 s b s_{b} sb​之后采取动作 a 2 a_{2} a2​之后得到的奖励是0,因此有:
    Q π ( s b , a 2 ) = 1 + 1 + 1 + 1 + 1 + 1 8 = 3 4 Q^{\pi}(s_{b},a_{2})=\frac{1+1+1+1+1+1}{8}=\frac{3}{4} Qπ(sb​,a2​)=81+1+1+1+1+1​=43​
    因为我只是简单的做了一个修改,计算的结果和上面的那个是一样的,当然这八条样本里面在观测到状态 s b s_{b} sb​ 之后都采取了动作 a 2 a_{2} a2​,实际更为复杂多变,actor不一定在观测到状态 s b s_{b} sb​都采取动作 a 2 a_{2} a2​的,还有别的动作可以采用。
  3. 两个价值函数之间的关系
    假设actor在观测到状态 s s s之后采取的动作是 a a a ,从环境里面得到的奖励是 r r r,因为做出动作 a a a 之后,导致环境发生变化,或者环境自身发生变化,状态转移到 s ′ {s}' s′ ,那么就有 Q π ( s , a ) = E [ r + V π ( s ′ ) ] (3) Q^{\pi}(s,a)=E[r + V^{\pi}({s}')]\tag{3} Qπ(s,a)=E[r+Vπ(s′)](3)
    公式解释:上面说了 Q π ( s , a ) Q^{\pi}(s,a) Qπ(s,a)是actor在观测到状态 s s s 确定采取动作 a a a 之后所获得的奖励的期望,而在状态 s s s 之后的状态我们却是不得而知的,因此上面的等式是成立的。

三、A2C算法推导

3.1、算法流程

由公式(2)我们有
G t n = ∑ t ′ = t T n γ t ′ − t r t ′ n (2) G_{t}^{n} = \sum_{{t}'=t}^{T_{n}}\gamma ^{{t}'-t}r_{{t}'}^{n}\tag{2} Gtn​=t′=t∑Tn​​γt′−trt′n​(2)
我们根据上面的两个值函数可以得到 G t n G_{t}^{n} Gtn​的期望是
E [ G t n ] = Q π ( s t n , a t n ) (4) E[G_{t}^{n}] = Q^{\pi}(s_{t}^{n},a_{t}^{n})\tag{4} E[Gtn​]=Qπ(stn​,atn​)(4)
公式(3)的解释:为什么会成立?
由公式(2)可知, G t n G_{t}^{n} Gtn​是奖励的累加和,因为奖励是actor观测到某个状态之后由环境反馈过来的,因次这和状态动作价值函数是一样的。主要是因为actor得到的奖励都是在确定所采取的动作之后才得到的,而状态动作价值函数 Q π ( s , a ) Q^{\pi}(s,a) Qπ(s,a)也是在actor观测到状态 s s s采取动作 a a a之后所获得奖励的期望。因此公式(3)成立。

我们可以使用 V π θ ( s t n ) V^{\pi_{\theta}}(s_{t}^{n}) Vπθ​(stn​) 来估计公式(1)中的baseline b b b,也就是 b = V π θ ( s t n ) (5) b = V^{\pi_{\theta}}(s_{t}^{n})\tag{5} b=Vπθ​(stn​)(5)
将公式(1),(4),(5)结合起来于是公式(1)就变为下面的公式(6).
▽ R ˉ θ = 1 N ∑ n = 1 N ∑ t = 1 T n ( Q π ( s t n , a t n ) − V π θ ( s t n ) ) ▽ l o g p θ ( a t n ∣ s t n ) (6) \bigtriangledown \bar{R}_{\theta } = \frac{1}{N}\sum_{n=1}^{N}\sum_{t=1}^{T_{n}}(Q^{\pi}(s_{t}^{n},a_{t}^{n})-V^{\pi_{\theta}}(s_{t}^{n}))\bigtriangledown logp_{\theta }(a_{t}^{n}|s_{t}^{n})\tag{6} ▽Rˉθ​=N1​n=1∑N​t=1∑Tn​​(Qπ(stn​,atn​)−Vπθ​(stn​))▽logpθ​(atn​∣stn​)(6)

但是公式(6)会引入一个问题,我们使用神经网络来估计 Q π ( s t n , a t n ) Q^{\pi}(s_{t}^{n},a_{t}^{n}) Qπ(stn​,atn​) 和 V π θ ( s t n ) V^{\pi_{\theta}}(s_{t}^{n}) Vπθ​(stn​),但是我们可以看到这两个函数的自变量是不一样的,也就是如果使用公式(6)来作为梯度更新的公式的话,这会引入两个神经网络,那么模型的复杂度也就会增加,复杂的模型会导致一系列问题,比如过拟合,两个值函数估测不准的可能性也会增加等等。

问题:我们是否可以只引入一个神经网络来解决这个问题?
答案是显然的,因为公式(6)里面有两个值函数,上面的(3)描述了这两个值函数之间的关系,我们要使用公式(3)来简化问题。
由公式(3)我们有 Q π ( s , a ) = E [ r + V π ( s ′ ) ] (3) Q^{\pi}(s,a)=E[r + V^{\pi}({s}')]\tag{3} Qπ(s,a)=E[r+Vπ(s′)](3)
公式(3)只是提供了理论上的可能性,但是计算期望是一件很麻烦的事情,而且在无穷多的情况下,计算机在实操时根本无法计算,那么我们就需要简化问题,于是有 Q π ( s , a ) = r + V π ( s ′ ) (7) Q^{\pi}(s,a)=r + V^{\pi}({s}')\tag{7} Qπ(s,a)=r+Vπ(s′)(7)
公式解释:状态 s ′ {s}' s′ 是actor观测到状态 s s s 时采取动作 a a a之后,状态转移到了状态 s ′ {s}' s′,这种状态的转移可能是由于actor所采取的动作导致的,也可能是环境本身自己发生了变化而发生了转移。

将公式(7)带入到公式(6),于是就有:
▽ R ˉ θ = 1 N ∑ n = 1 N ∑ t = 1 T n r t n + V π θ ( s t + 1 n ) − V π θ ( s t n ) ▽ l o g p θ ( a t n ∣ s t n ) (8) \bigtriangledown \bar{R}_{\theta } = \frac{1}{N}\sum_{n=1}^{N}\sum_{t=1}^{T_{n}}r_{t}^{n}+V^{\pi_{\theta}}(s_{t+1}^{n})-V^{\pi_{\theta}}(s_{t}^{n})\bigtriangledown logp_{\theta }(a_{t}^{n}|s_{t}^{n})\tag{8} ▽Rˉθ​=N1​n=1∑N​t=1∑Tn​​rtn​+Vπθ​(st+1n​)−Vπθ​(stn​)▽logpθ​(atn​∣stn​)(8)

公式(8)解释:虽然上面是说使用 V π θ ( s t n ) V^{\pi_{\theta}}(s_{t}^{n}) Vπθ​(stn​)来作为baseline,但是我更加觉得这个是在使用 V π θ ( s t + 1 n ) − V π θ ( s t n ) V^{\pi_{\theta}}(s_{t+1}^{n})-V^{\pi_{\theta}}(s_{t}^{n}) Vπθ​(st+1n​)−Vπθ​(stn​)来逼近环境给出的奖励 r t n r_{t}^{n} rtn​.我对这个公式的直观理解有点描述不出来,感觉和TD方法的思想有点类,从公式可以看出,计算这个梯度只需要使用 ( s t n , a t n , r t n , s t + 1 n ) (s_{t}^{n},a_{t}^{n},r_{t}^{n},s_{t+1}^{n}) (stn​,atn​,rtn​,st+1n​)这样的四元组,因为我们在采样数据的时候只需要存储这样的四元组就可以了,和TD方法的思想是一样的,我觉得是可以实现单步更新,无需等待一个回合结束再更新模型的。我们知道 r t n r_{t}^{n} rtn​是一个random variable(r.v.),因此公式里面还是引入了具有随机性的东西,只有在考虑的期望的时候才是正确的,但是 r t n r_{t}^{n} rtn​的随机性相对于 G t n G_{t}^{n} Gtn​的随机性是小了很多,因为 G t n G_{t}^{n} Gtn​是各个时间步奖励的和,因为相对来说,随机性不大。

3.2、两个tips

  1. 参数共享
    我们知道状态价值函数和策略网络的输入都是状态,但是输出是不一样的,因此我们可以将一些参数共享来减少模型的复杂性。
  2. 探索
    我们为了避免actor陷入一个局部最优解,我们要增加actor的探索能力,也就是去尝试做出更多的动作,可以是策略网络输出的动作的分布更加均匀一点,这样可以每个动作都有被优化到的可能性。例如可以使用e-greedy算法增加模型的探索能力,有一定的随机性选择新的动作。

四、A3C

也就是多个actor和环境互动,并行更新网络

最上面的那个网络我们认为是global network,然后将他的参数复制,弄出多个网络,然后数据,计算梯度,更新global network。
算法流程:

1、复制网络参数生成多个网络,放在不同的GPU上
2、每个网络都和环境互动,生成数据
3、计算梯度
4、更新global network

五、A2C的Pytorch实现

import random
import numpy as np
import gym
import torch.nn as nn
import torch as t
from torch.nn import functional as F
import matplotlib.pyplot as plt
import os# env.close()eplison = 0.1
criterion = nn.CrossEntropyLoss()
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
gamma = 0.9
actor_lr = 0.001
critic_lr = 0.01
env = gym.make("CartPole-v0")
env.seed(1)     # reproducible, general Policy gradient has high variance
env = env.unwrapped
batch_size = 1
epochs = 500class Share_layer(nn.Module):def __init__(self):super(Share_layer, self).__init__()self.linear1 = nn.Linear(4, 20)nn.init.normal_(self.linear1.weight, 0, 0.1)nn.init.constant_(self.linear1.bias, 0.1)def forward(self, out):out = self.linear1(out)out = F.relu(out)return outclass Actor(nn.Module):def __init__(self, sl):super(Actor, self).__init__()self.share_layer = slself.linear2 = nn.Linear(20, 2)nn.init.normal_(self.linear2.weight, 0, 0.1)nn.init.constant_(self.linear2.bias, 0.1)def forward(self, x):out = t.from_numpy(x).float()out = self.share_layer(out) out = self.linear2(out)prob = F.softmax(out, dim = 1) #这个输出主要是用来使用概率来挑选动作return prob, outclass Critic(nn.Module):def __init__(self, sl):super(Critic, self).__init__()self.share_layer = slself.linear2 = nn.Linear(20, 1)nn.init.normal_(self.linear2.weight, 0, 0.1)nn.init.constant_(self.linear2.bias, 0.1)def forward(self, x):out = t.from_numpy(x).float()out = self.share_layer(out)out = self.linear2(out)return out#下面该函数用来选择动作
def choose_action(prob):# print(prob)action = np.random.choice(a = 2, p = prob[0].detach().numpy())    return action# action = np.random.choice(a = 6, p = prob[0].detach().numpy())# v = random.uniform(0, 1)# p, index = t.topk(prob, 1, dim = 1)# #下面开始eplison-greedy 算法# if v > eplison:       #     #这里是求最大的状态价值函数对应的动作#     action = index[0][0].item()        # else: #     #下面是随机产生动作#     action = random.randint(0, 1)  # return action#下面的函数主要用来计算Actor部分训练的损失函数
def Actor_learn(optim, critic, s, s_, a, r, logits):'''根据当前的状态,奖励以及下一个时间步的章台完成损失函数的计算Parameters----------critic : Critic()实例用来估计当前时间的奖励的期望s : float array当前时间步的状态.a : int scalar当前时间步根据当前状态s所采取的动作r : float scalar当前时间步的奖励.s_ : float array下一个时间步的状态.logits : actor网络最后一层的输出用来计算交叉熵损失Returns-------Actor 的损失.'''V_s = critic(s)V_s_ = critic(s_)#开始计算动作对应的交叉熵a = t.tensor([a]).long()logp_a = criterion(logits, a)l = r + V_s_ - V_sl = l.item()loss = l * logp_a# print('Actor loss is :', loss)optim.zero_grad()loss.backward()optim.step()def Critic_learn(optim, critic, s, r, s_):'''用来计算Critic网络的损失,来更新Critic网络Parameters----------critic : Critic实例用来计算各个时间步的值函数.s : float array当前时间步的状态.r : float scalar当前时间步的奖励.s_ : float array下一个时间步的状态.Returns-------Critic网络的损失.'''V_s = critic(s)V_s_ = critic(s_)loss = (r + gamma * V_s_.item()- V_s)**2optim.zero_grad()loss.backward()optim.step()return r + gamma * V_s_.item()- V_sdef learn():#下面采用时间差分方法学习,该方法的学习速度较快,且很稳,时间差分方法和蒙特卡洛方法各有自己的优势sl = Share_layer()actor = Actor(sl)critic = Critic(sl)actor.train()critic.train()#还需要两个优化器actor_optim = t.optim.Adam(actor.parameters(), lr = actor_lr)critic_optim = t.optim.Adam(critic.parameters(), lr = critic_lr)train_rewards = []for i in range(epochs):state = env.reset()done = Falsesum_rewards_i = 0step = 0while not done:step += 1env.render()state = np.expand_dims(state, axis = 0)prob, logits = actor(state)#下面开始选择动作action = choose_action(prob)state_, reward, done, info = env.step(action)if done:reward = -20.0sum_rewards_i += reward#下面开始学习,先学习的是critic网络,接着才是actor网络l = Critic_learn(critic_optim, critic, state, reward, state_)Actor_learn(actor_optim, critic, state, state_, action, reward, logits)                       state = state_train_rewards.append(sum_rewards_i)print('epoch is :', i)print('step nums is :', step)plt.plot(train_rewards)if __name__ == "__main__":learn()    env.close()

参考内容主要是李宏毅老师的强化学习课程和莫烦python的代码

强化学习算法A2C(Advantage Actor-Critic)和A3C(Asynchronous Advantage Actor-Critic)算法详解以及A2C的Pytorch实现相关推荐

  1. c linux time微秒_学习linux,看这篇1.5w多字的linux命令详解(6小时讲明白Linux)

    用心分享,共同成长 没有什么比每天进步一点点更重要了 本篇文章主要讲解了一些linux常用命令,主要讲解模式是,命令介绍.命令参数格式.命令参数.命令常用参数示例.由于linux命令较多,我还特意选了 ...

  2. 【ros学习】14.urdf、xacro机器人建模与rviz、gazebo仿真详解

    一.起因 学校的这学期课程是ros机器人开发实战,我们学习小组也要搞一个自己的机器人模型,我们组又叫葫芦组,所以我就做了个葫芦形状的机器人,虽说有点丑,本来想用maya建模再导入的,奈何不太懂maya ...

  3. 【java学习之路】(java框架)004.Mybatis SQL映射文件详解

    02Mybatis SQL映射文件详解 ​ 在之前我们学习了mybatis的全局配置文件,下面我们开始学习mybatis的映射文件,在映射文件中,可以编写以下的顶级元素标签: cache – 该命名空 ...

  4. 【深度学习】跟李沐学ai 线性回归 从零开始的代码实现超详解

    目录 一.引言 二.本文代码做了什么 如何利用数据集训练 三.代码实现与解析 一.导包 二.相应的函数实现 1 生成样本(数据集) 2 按批量读取数据集 3 定义模型 损失函数 算法 1 定义模型 2 ...

  5. Git学习系列之Windows上安装Git之后的一些配置(图文详解)

    不多说,直接上干货! 前面博客 Git学习系列之Windows上安装Git详细步骤(图文详解) 第一次使用Git时,需要对Git进行一些配置,以方便使用Git. 不过,这种配置工作只需要进行一次便可, ...

  6. 项目管理学习总结(9)——史上最全互联网八大技术岗位详解

    互联网技术岗位详解,涉及到前段开发.后端开发.移动端开发.大数据.项目管理.测试.运维.技术管理等八大领域. 架构师 每个产品线都有架构师,在技术平台部门也需要技术平台的架构师. 架构师负责设计系统整 ...

  7. 【大道至简】机器学习算法之隐马尔科夫模型(Hidden Markov Model, HMM)详解(3)---学习问题:Baum-Welch算法推导及Python代码实现

    ☕️ 本文系列文章汇总: (1)HMM开篇:基本概念和几个要素 (2)HMM计算问题:前后向算法 (3)HMM学习问题:Baum-Welch算法 (4) HMM预测问题:维特比算法 ☕️ 本文来自专栏 ...

  8. 算法学习FFT系列(2):快速数论变换NTT bzoj3992: [SDOI2015]序列统计例题详解

    bzoj3992: [SDOI2015]序列统计 Description 小C有一个集合S,里面的元素都是小于M的非负整数.他用程序编写了一个数列生成器,可以生成一个长度为N的数列,数列中的每个数都属 ...

  9. 图像处理知多少?准大厂算法工程师30+场秋招后总结的面经问题详解

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 作者丨灯会 来源丨极市平台 编辑丨极市平台 极市导读 本篇主要包含了图像滤波.边缘检测相关常考内容等相 ...

最新文章

  1. mysql 数据库的导入和导出
  2. 如何在node.js中发出HTTP POST请求?
  3. ASP.NET Web API中的参数绑定总结
  4. 全文检索、数据挖掘、推荐引擎系列5---文章术语向量表示法
  5. 花小猪,真正的对手是谁?
  6. web视图层:JSP
  7. js的异常捕获try和catch语句
  8. 计算机图形学 实现鼠标拖拽图元
  9. windows server 2003优化设置方法
  10. android MVC和MVP探讨
  11. pythonindex函数的使用格式_Python正课31 —— 函数参数的使用
  12. Java程序设计 -- 常用API(上)(九)
  13. R语言使用dplyr包计算dataframe分组聚合样本独特值个数、计数个数、四分位距IQR
  14. 珠宝行业电子秤串口程序开发
  15. 微信相关开发问题收集
  16. Java并发编程模拟管程(霍尔Hoare管程、汉森Hansan管程、MESA管程)
  17. 喜欢游戏的人,福利来啦,这个岗位可以边打游戏边工作
  18. 微信H5支付,HTML页面代码
  19. 支付宝公众服务接口和微信公众平台接口的区别
  20. 图片转码 webp 转 png、jpg

热门文章

  1. springboot 微信太阳码_springboot教务管理系统+微信小程序云开发
  2. 黑客登录界面科幻黑色主题网页模板表单验证代码
  3. QGIS基本功|5 QGIS图层进阶(二)- 连接属性表
  4. 生成token和验证token机制
  5. 教你1个转换小技巧,免费多张jpg合并成pdf
  6. 接口测试基础、流程、工具
  7. 文本匹配(语义相似度/行为相关性)技术综述
  8. 宏观经济需求与供给分析之:需求的收入效应、替代效应和需求定律
  9. Logical Architecture
  10. Live555源码阅读笔记(一):源码介绍文档 及 源码目录结构