1 Reinforcement Learning是什么?
……此处省略,简单的介绍,大家各自找本书看看即可。大家只要知道AlphaGo、XLand、绝悟、Tesla FSD等等这些都是运用了RL方法的就行了。我主要拿它来做不确定环境下的自适应决策了,然后抄机器人的作业,因为我懒。下面我随意展示了一张网图是应用RL方法的。看书,我当然还是推荐sutton那本Reinforcement Learning introduction。

2 例子
2.1 Agent分析
首先尝试一个定义environment强化学习环境的例子:例子是Agent在环境中行动有限的步数,获得随机的奖励。
class Enviroment:
def __init__(self):
self.steps_left = 10 # 初始化了Agent可以在环境中与环境互动10次
def get_observation(self):
# 这里环境的状态是一个计数器,限制Agent与环境交互的次数
return [0.0, 0.0, 0.0] # 这个例子里观察向量始终为0.0,环境无内部状态
def get_actions(self):
# 这里设定允许Agent请求的行为集
return [0, 1] # 例子场景Agent只有两种行为
def is_done(self):
# 这里定义一个回合(episode)是否结束
# 类似玩游戏一局结束,环境给出信号Game Over
return self.steps_left == 0
def action(self, action):
# action函数是环境的核心功能部分
# 它主要完成两个功能:
# 1. 根据Agent的行为,返回奖励值
# 2. 根据环境状态,提示游戏是否还能继续进行,即是否is_done
if self.is_done():
raise Exception("Game is Over")
self.steps_left -= 1
return random.random() # 如果还能继续,返回随机奖励
定义完环境,然后就是定义智能体Agent。
class Agent:
def __init__(self):
self.total_reward = 0.0 # 初始化了一个Agent获取了多少奖励的公共属性
def step(self, env): # 构造Agent行为动态,传入环境实例
current_obs = env.get_observation() # 获取环境状态
actions = env.get_actions() # 获取行为集
reward = env.action(random.choice(actions)) # 选择行为并兑现环境对行为的反馈
self.total_reward += reward # 记录行为奖励结果
上面两个类再配合上下面的main方法,一个小的RL例子就完成了。运行多次,可以得到Agent在这个随机环境中获得的不同奖励之和。
if __name__ == "__main__":
env = Environment()
agent = Agent()
while not env.is_done():
agent.step(env)
print('总奖励: %.4f' % agent.total_reward)
2.2 硬件和软件配置
-
GPU: 适配CUDA的显卡,这个肯定越贵体验越好。 -
Numpy: 这个做数据分析的都知道,Anaconda下载安装后自然有。 -
OpenCV: 做图像处理的。要做玩游戏的机器人,处理画面用得多。 -
Gym: 做RL的一个环境。当然我自己平时做的场景Gym上是没有的,大家可以在Gym上做练习。 -
PyTorch: 做深度学习的框架,看个人喜好,我自己喜欢用这个。Tensorflow等其他的框架也没毛病。 -
Ptan: 这个是Gym的一个扩展包。
2.3 OpenAI Gym API
这个东东学习RL基本都会用到。不过仅限学习的时候用,很多具体的场景,还是得自己去开发的。这玩意儿等于给大家提供了个环境集合。注意是环境集合。
每个具体的环境又提供了用于做RL实验环境所必备的几大要件:行为空间,观察空间(状态空间)以及step()行为执行方法和行动终止、场景初始化等。
-
行为空间(Action Space):主要分离散型和连续型。离散型很简单,一般就像前面的例子给出几个行为选项再编码。而连续型一般用的场景比如智能驾驶方向盘转动的角度可以理解为连续型变量。通常对于连续型行为空间只给出行为边界。如转动角度的范围。
-
观察空间(Observation Space): 这个主要是环境反馈给Agent的可观察的结果。就好比一个走迷宫的机器人每次移动后要能观察到自己所处的位置类似。
-
step()和reset():一个是Agent行为执行,一个是环境重置。
大家可以把Gym装好后,看看官方文档里的例子,很多书也会拿CartPole那个场景来举例子,我这里就不说了。玩玩官方给的东西,大家就懂了。
3. 深度学习pytorch
3.1 创建tensor
pytorch的张量创建、索引、切片等方式与numpy很相似,熟悉numpy的随便过下即可。
import torch
import numpy as np
a = torch.FloatTensor(3, 2) # 这个是未初始化的tensor
a.zero_() # 初始化为0
注意pytoch里的方法运算有两种:inplace和functional。inplace通常名称后有下划线,直接改变a的内容。而functional的方法只改变a的表现形式,但a本身不改变,类似深浅copy。
torch.FloatTensor([[1, 2, 3], [3, 2, 1]])
n = np.zeros(shape=(3, 2))
b = torch.tensor(n)
注意以前 pytorch0.4.0 版本之前推荐 torch.from_numpy() 做array到tensor的转换;0.4.0版本之后推荐用更加灵活的 torch.tensor() 方法。
此外,pytorch0.4.0 版本之后支持0维度tensor,也即是标量(scalar tensor)。
3.2 tensor数据的运算
主要还是参考官方文档吧, http://pytorch.org/docs/。比如把里面的一些功能性方法搞明白即可,比如:
-
torch.stack() -
torch.transpose() -
torch.cat() -
…
这部分还是非常简单的。
3.3 tensor在GPU上运算
pytorch支持GPU运算,那么自然有两个版本。个人肯定推荐GPU版本,但你得有Nvidia的显卡。主要用到的pkg也就是torch.cuda。把一个tensor放到GPU上只需要to(device)一下即可。搭建网络的时候需要注意什么tensor是需要放到GPU上去提升效率的。通常有放上去的有:
-
输入信息 -
loss -
网络net(本质放上去的是网络中需要计算梯度的参数)
a = torch.FloatTensor([2, 3])
ca = a.cuda() # ca : tensor([2., 3.], device='cuda:0')
a + 1 # tensor([3., 4.])
ca + 1 # tensor([3., 4.], device='cuda:0')
to()方法和torch.device是0.4.0版本之后引入的,过去CPU和GPU之间的传递主要依靠a.cpu()a.cuda()。有了to(device),可以在创建tensor的时候直接做传递。 当然a.cpu()a.cuda()虽然过时了,但还是可以用的,目前兼容。
3.4 梯度 Gradients
梯度本身没什么好讲的,学过高等数学的应该都知道。没学过的也可以把他理解为衡量两个变量之间的变化关系的东东。优化理论里面常沿着梯度的方向去寻找极值。这个也是深度网络调参的基石。

目前主流的深度学习框架,按照梯度的计算方式可以分为两类:
-
静态图:顾名思义,计算前肯定是先把网络前向计算流结构搭建好了。比如:tensorflow,theano -
动态图:这个相对灵活。pytorch, chainer等。主要动态图,更加 Pythonic。因此又被成为 notebook gradients 。
tensor的梯度属性
tensor的梯度相关属性(attributes):
-
grad: 这个代表该tensor的梯度,shape和tensor保持一致。 -
is_leaf: 这个表示这个tensor是用户创建的,还是网络图结构中间计算结果。用户创建的为True。 -
requries_grad: 指的是这个tensor是否需要计算梯度。当我们在transfer learning中需要固定部分已训练好的参数时,通常会把这部分网络结构的参数tensor的requires_grad设置为False。这样在新的网络结构中进行反向传播时就不会更新这部分参数。
v1 = torch.tensor([1., 1.], requires_grad=True)
v2 = torch.tensor([2., 2.]) # requires_grad 默认为False
v_sum = v1 + v2
v_res = (v_sum * 2).sum()
v_res # 返回 tensor(12.)
v1.is_leaf, v2.is_leaf # 返回(True, True)
v_sum.is_leaf, v_res.is_leaf # 返回(False, False)
v1.requires_grad # 返回 True
v2.requires_grad # 返回 False
v_sum.requires_grad # 返回 True
v_res.requires_grad # 返回 True
v_res.backward()
v1.grad # 返回 tensor([2., 2.])
注意 : 在pytorch 0.4.0之前的版本,对于需要梯度的tensor通常用Variable类来进行声明。但之后的版本直接用tensor的requires_grad内建属性来表明。或许大家在很多书的代码中还能看到Variable之类的表达,说明那个书已经有点旧了。不过没有关系,思想不会变,但如果测试那些书中的代码要注意。
3.5 神经网络结构块
通常我们在pytorch中搭建网络结构会用到的module为torch.nn。里面有很多成熟的网络结构框架,和基础的搭建网络结构的组件。既可以用别人搭建好的网络结构,甚至用别人pre-trained网络结构。当然也可以自己利用网络结构的组件搭建自己的网络结构。
总而言之,搭建网络结构就跟小孩子玩乐高一样简单。
import torch.nn as nn
l = nn.Linear(2, 5)
v = torch.randn(2)
print(l(v))
# 我的电脑返回如下:
# tensor([ 0.9199, 0.8783, 1.1828, -0.3686, 0.8874], grad_fn=<AddBackward0>)
torch.nn中所有的类都继承自nn.Module,这个是用来实现你自己网络结构类的更高级的base类。而nn.Module中被继承的一些特别重要的方法要理解。
-
parameters(): 这个是包含网络结构中所有参数Variables的迭代器,调用这个方法返回的也是这个迭代器。当然Variables是需要计算梯度的那些。 简单的就把他理解成包含module weights的一个容器。
-
zero_grad(): 这个方法也很重要,即将所有需要计算梯度变量的梯度初始化为0。后面反向传播的时候会经常用到。每一次反向传播调参前,梯度都要重置一遍。否则梯度会在前一个梯度上累积。
-
to(device): 这个不用说了,为了提升网络训练效率,我们可以把网络搬到GPU上,就要用到这个方法来搬。
-
state_dict(): 这个返回网络的各种参数。有兴趣的回头可以在debug的时候把你创建的网络打开看看就明白了。
-
load_state_dict(): 读取一些状态参数来初始化网络结构。
通常我们用Sequential() 来组建网络结构
s = nn.Sequential(
nn.Linear(2, 5),
nn.ReLU(),
nn.Linear(5, 20),
nn.ReLU(),
nn.Linear(20, 10),
nn.Dropout(p=0.3),
nn.Softmax(dim=1))
v = torch.randn(1, 2)
print(s(v))
# tensor([[0.0760, 0.0897, 0.0974, 0.0596, 0.1021, 0.1056, 0.0897, 0.1086, 0.1488,
# 0.1224]], grad_fn=<SoftmaxBackward>)
3.6 创建我们自己网络结构的基本框架
当然首先用一个网络类来定义我们自己的网络结构OurModule。
class OurModule(nn.Module):
def __init__(self, num_inputs, num_classes, dropout_prob=0.3):
super(OurModule, self).__init__() # 继承下nn.Module那些网络通用的方法
self.pipe = nn.Sequential(
nn.Linear(num_inputs, 5),
nn.ReLU(),
nn.Linear(5, 20),
nn.ReLU(),
nn.Linear(20, num_classes),
nn.Dropout(p=dropout_prob),
nn.Softmax(dim=1))
def forward(self, x): # 当然还得实现个网络前向计算的forward()方法
return self.pipe(x)
# 使用我们搭建的网络,肯定是将网络类实例化咯。
if __name__ == '__main__':
net = OurModule(num_inputs=2, num_classes=3)
v = torch.randn(1, 2)
out = net(v)
print(net)
print(out)
以上则是我们还没有加入损失函数、反向传播机制、优化参数等部分的一个简单框架。
3.7 加入损失函数和优化器
-
Loss Function: 损失函数通常根据我们优化目标来构造。当我们的网络结构类似于一个“拟合”工具的时候,我们通常让每次通过网络前向计算得到的输出结果 与之间的差。而这个差衡量了我们模型拟合效果的好坏。很显然优化目标就是让这两者差距更小,就有了比如说MSE方法的损失函数。通过反向传播计算损失函数Loss与网络结构中参数的梯度,就知道要使得Loss下降,参数应该如何调整。 pytorch提供了多种损失函数,常用的,比如:nn.MSELossnn.BCELossBCEWithLogitsnn.CrossEntropyLoss
-
optimizers: 优化器是用于反向传播计算参数梯度信息后对参数进行更新的组件。常用的优化器,比如:SGDRMSpropAdagrad
一个通用的损失优化调参的过程类似:
for batch_samples, batch_labels in iterate_batches(data, batch_size):
batch_samples_t = torch.tensor(batch_samples)
batch_labels_t = torch.tensor(batch_labels)
out_t = net(batch_samples_t)
loss_t = loss_function(out_t, batch_labels_t)
loss_t.backward()
optimizer.step()
optimizer.zero_grad() # 优化一次后,带优化的参数梯度要清零,否则会累积
3.8 如何通过tensorboard跟踪训练过程
TensorBoard原本是TensorFlow上的一个跟踪训练结果的工具。当然现在pytorch也是能用的。TensorBoard本身是一个web service,用来展示传入的数据信息。我们只需要把训练过程中想要记录的信息传入,自然就可以在TensorBoard中以网页的形式展示出来。
使用方法,整个过程跟画图plot流程相似,加入数据,展示数据,用之前搞清楚SummaryWriter中各种方法的参数即可。
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("logs") # 标识处存储的位置,文件夹 logs
# writer.add_image() # 这种是加图像
# writer.add_scalar() # 添加标量数据
for i in range(100):
writer.add_scalar(tag='y=x', scalar_value=i, global_step=i)
# 展示的时候scalar_value对应y轴,global_step为x轴
writer.close()
如果没有,如何安装tensorboard工具呢?那是非常简单
pip install tensorboard
如何打开SummaryWriter生成好的事件文件在tensorboard中展示呢?
>tensorboard --logdir=logs
tensorboard 2.1.0 at http://localhost:6006/
>tensorboard --logdir=logs --port=6007
tensorboard 2.1.0 at http://localhost:6007/

上图就是例子在tensorboard中的展示结果,tensorboard提供了很多工具按钮可以做一些简单的变换观察。大家自己随便摸索下吧。
如果要往tensorboard中写入一些图像怎么做?
import numpy as np
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
image_path = 'data/image.jpg'
writer = SummaryWriter('logs')
img_PIL = Image.open(image_path)
img_array = np.array(img_PIL)
writer.add_image('test', img_array, 1, dataformats='HWC')
# 这里注意图片channel维度的位置
# 如果我们图片的张量信息维度为 HWC,必须在dataformats中明示给SummaryWriter
writer.close()
展示出来的结果就是下面这个样子。

3.9 补充一些pytorch中数据处理transform方法
引用pytorch中自带的transform方法:
from torchvision import transforms
# 其实所有的transform方法都以各种类class的方式封装在transforms.py中
import numpy as np
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
image_path = 'data/image.jpg' # 当然你要用绝对路径也是可以的
img = Image.open(image_path)
# 这个时候img是PIL的图片类型
img2tensor = transforms.ToTensor()
tensor_img = img2tensor(img)
# 这个时候tensor_img就是tensor类型
前面在使用writer.add_image()时传入的时numpy.array类型传入的,我们在参数中指明了dataformats=’HWC’。 如果不用PIL,用OpenCV也是可以的。我们将numpy.array格式的图片转换成tensor也是可以传入writer.add_image()的。
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
import cv2
img_path = 'data/image.jpg'
cv_img = cv2.imread(img_path)
writer = SummaryWriter('logs')
tensor_trans = transforms.ToTensor()
tensor_img = tensor_trans(cv_img)
print(tensor_img.shape)
# 这里返回的是 torch.Size([3, 1080, 1440]) 我放的图是1080*1440的
# channel这时到高维度去了,跟PIL读取的不一样
writer.add_image('tensor_img', tensor_img)
writer.close()
上面这段代码实现的效果和前面用PIL做的一样。
下面还有几种关于图片的变换transform方法。包括:compose() 等等
# 注意很多transform方法内建了__call__()方法,可以直接调用
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from PIL import Image
import cv2
import numpy as np
img = cv2.imread('data/image.jpg')
writer = SummaryWriter('logs')
trans_2tensor = transforms.ToTensor()
tensor_img = trans_2tensor(img)
writer.add_image('2tensor', tensor_img)
# Normalize
trans_norm = transforms.Normalize([0.2, 0.3, 0.5], [0.5, 0.4, 0.2], inplace=False)
img_norm = trans_norm(tensor_img)
writer.add_image('Normalize', img_norm, 2)
# Resize
trans_resize = transforms.Resize((218, 218))
# img_resize = trans_resize(tensor_img) # resize这里必须是PIL image传入
img = Image.open('data/image.jpg')
print(np.array(img).shape) # 在这一步图片的尺寸为(1080, 1440, 3)
img_resize = trans_resize(img) # resize PIL-->>PIL
img_resize = trans_2tensor(img_resize)
print(img_resize.shape) # 这一步图片的尺寸torch.Size([3, 218, 218])
# 所以这里resize过程中实现了将低维度的3通道,提到了高维度
writer.add_image('Resize', img_resize, 3)
# compose
trans_resize2 = transforms.Resize((1024, 768))
trans_compose = transforms.Compose([trans_resize2, trans_2tensor])
img_resize2 = trans_compose(img) # 以trans_resize2的输入类型为准,所以PIL
writer.add_image('Compose', img_resize2, 4)
# randomCrop
trans_random = transforms.RandomCrop(512)
trans_compose2 = transforms.Compose([trans_random, trans_2tensor])
for i in range(20):
img_crop = trans_compose2(img)
writer.add_image('RandomCrop', img_crop, i)
writer.close()
同样的大家可以在tensorboard中去自己尝试torchvision.transforms这个pkg中提供的转换方法。需要注意的是使用时候一定关注transform方法的输入和输出类型,这个可以在transforms.py源码文件中看到。 下图是随机裁剪normalize和randomCrop例子在tensorboard中的尝试结果。

原文始发于微信公众号(拒绝拍脑袋):【授人以渔】RL系列(1)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/54895.html