开悟初赛笔记-特征处理篇

开悟初赛笔记-特征处理篇

本次开悟初赛期对于特征的处理要求有一点高,而且不同赛道的赛道地图和特征信息都不大相同。

比如博弈赛道的地图里面英雄的局部视野域都是11 x 11 的矩阵,在具身赛道里面的地图英雄的局部视野域就变成了51 x 51 的矩阵了。

不同的局部视野域做特征输入需要就特征的维度长度来进行不同的模型设计,而且不同赛道也需要采取不同的特征设计。

比如具身赛道的51 x 51展平之后维度过于庞大,应当先做卷积处理,而博弈赛道的话没必要特意做卷积处理,用MLP即可,还有博弈的中级和高级赛道的organs字段里面是不一样的,具身的话基本只存在视野域内的organ。

以及官方默认提供的模型输出之后的维度仅仅只有移动的八维,闪现的八维是没有进行提供的。

这会导致智能体在面对这种有随机障碍物的环境很难去到达终点,所以我们做的第一步是做模型输出的动作空间进行扩展

注:本人也是在历史各位大佬的帖子里面不断学习成长过来的,初赛的话发这个帖子是希望无论是新来参加开悟的还是以前一起的都能够一起讨论学习,我已经参加了大概二年开悟比赛了,从一开始的小白完全不懂,到现在也能够自己发帖帮助一些萌新。我的话不在乎成绩,我很珍惜和各位学习的这些时间。

一、先让模型学会闪现!

初赛的环境障碍物是随机的,为了能够让智能体快速到达终点,闪现的动作是必须的。

具体的修改是扩展动作维度从原来的8维扩展到16维

具体的修改是在feature文件夹下的preprocessor.py文件里面,里面包含了关于特征处理的所有部分,默认特征处理函数接受到的参数是frame_state,里面包含了obs和extra_info参数,extra_info在特征处理是不允许使用的,但是奖励设计是可以参与的。因为每次进行评估的过程中,extra_info是空的,但是训练阶段extra_info是可以取到的,所以可以利用奖励设计。

闪现的具体修改细节如下

  • 修改preprocessor.py里面 Preprocessor类的初始化
class Preprocessor:
def __init__(self) -> None:
# 从8修改成16,代表8个移动维度,8个闪现维度
self.move_action_num = 16
self.reset()
def reset(self):
...
# 闪现是否可用
self.is_flashed = True

def pb2struct(self, frame_state, last_action):
obs, _ = frame_state
self.step_no = obs["frame_state"]["step_no"]

hero = obs["frame_state"]["heroes"][0]
map_info = obs["map_info"]

# 闪现是否可用 -- 新增一个字段来代表闪现是否可用,默认初始化的时候可以为True
if hero['talent']['status'] == 0:
self.is_flashed = False
elif hero['talent']['status'] == 1:
self.is_flashed = True
  • 然后在官方提供的获取合法动作的函数里面加入最新的闪现的8个合法动作维度
def get_legal_action(self):
# if last_action is move and current position is the same as last position, add this action to bad_move_ids
# 如果上一步的动作是移动,且当前位置与上一步位置相同,则将该动作加入到bad_move_ids中

legal_action = [self.move_usable] * self.move_action_num

# 添加闪现的合法性
if self.is_flashed:
legal_action[8:] = [True] * 8
else:
legal_action[8:] = [False] * 8

if (
abs(self.cur_pos_norm[0] - self.last_pos_norm[0]) < 0.001
and abs(self.cur_pos_norm[1] - self.last_pos_norm[1]) < 0.001
and self.last_action > -1
):
self.bad_move_ids.add(self.last_action)
else:
self.bad_move_ids = set()

for move_id in self.bad_move_ids:
legal_action[move_id] = 0

if self.move_usable not in legal_action:
self.bad_move_ids = set()
legal_action[:8] = [self.move_usable] * 8

return legal_action
  • 修改conf.py的配置文件
class Config:
...
# actions
# 动作
ACTION_LEN = 1
# 8个移动方向 + 8个闪现方向
ACTION_NUM = 8 + 8

# features
# 特征
FEATURES = [
#2 当前位置的归一化
2,
#6 归一化的终点位置特征
6,
#6 归一化的历史位置特征
6,
#16维 移动8方向的合法动作,闪现8方向的合法动作
8 + 8,
]

通过这些基础的修改和官方默认的奖励,最终应该可以然后训练的胜率(监控指标的diy_1)可以稳定在0.98-0.99左右徘徊,基本稳定接近到1.0

二、其他特征解析

关于其他的特征,我们可以通过详细分析下官方的obs参数里面有什么

message Observation {
FrameState frame_state = 1; // 局部环境数据
ScoreInfo score_info = 2; // 得分信息
repeated MapInfo map_info = 3; // 局部地图信息
repeated int32 legal_act = 4; // 合法动作
}

message FrameState {
int32 step_no = 1; // 步数
repeated RealmHero heroes = 2; // 英雄状态
repeated RealmOrgan organs = 3; // 物件状态
}

message ScoreInfo {
float score = 1; // 即时得分
float total_score = 2; // 总得分
int32 step_no = 3; // 步号
int32 treasure_collected_count = 4; // 收集到的宝箱数量
int32 treasure_score = 5; // 收集到的宝箱得分
int32 buff_count = 6; // 收集到的buff数量
int32 talent_count = 7; // 使用技能的数量
}

message RealmOrgan {
int32 sub_type = 1; // 物件类型,1代表宝箱, 2代表加速buff,3代表起点,4代表终点
int32 config_id = 2; // 物件id 0代表buff,1~13代表宝箱 21代表起点, 22代表终点
int32 status = 3; // 0表示不可获取,1表示可获取, -1表示视野外
Position pos = 4; // 物件位置坐标
int32 cooldown = 5; // 物件剩余冷却时间
RelativePosition relative_pos = 6; // 物件相对位置
}

#其中0表示不可通行,1表示可以通行,2表示起点位置,3表示终点位置,4表示宝箱位置,6表示加速增益位置。
message MapInfo {
repeated int32 values = 1; // 地图信息行信息
}

通过上面官方给出的数据协议,我们其实可以设计如下几个基础的特征,可能可以一定程度上提高模型的性能。

① 智能体的局部视野域

智能体的局部视野域的设计可以利用obs参数里面的map_info进行设计,而且我们已知的是地图里面有不同的数字表示不同的视野信息,我们可以把它分割成对应四个视野域信息

注:演示的是博弈赛道里面的,具身赛道请根据自己赛道的情况去设计视野域的大小,具身是51 x 51

# 其中0表示不可通行,1表示可以通行,2表示起点位置,3表示终点位置,4表示宝箱位置,6表示加速增益位置。
treasure_map = np.zeros((11, 11), dtype=np.float32)
end_map = np.zeros((11, 11), dtype=np.float32)
obstacle_map = np.zeros((11, 11), dtype=np.float32)
buff_map = np.zeros((11, 11), dtype=np.float32)
# 遍历 map_info 并填充矩阵
for r, row_data in enumerate(map_info):
for c, value in enumerate(row_data['values']):
# 宝箱
if value == 4:
treasure_map[r, c] = 1.0
# 终点
elif value == 3:
end_map[r, c] = 1.0
# 障碍物
elif value == 0:
obstacle_map[r, c] = 1.0
# 加速增益
elif value == 6:
buff_map[r, c] = 1.0

# 展平所有矩阵
self.treasure_flag = treasure_map.flatten()
self.end_flag = end_map.flatten()
self.obstacle_flag = obstacle_map.flatten()
self.buff_flag = buff_map.flatten()

另外的,虽然官方没有给出对应的记忆矩阵视野域,但是我们其实可以自己去根据每次的位置去做更新自己设计一个记忆矩阵的视野域。

# 首先一样的初始化
self.global_memory_map = np.zeros((128,128), dtype=np.float32)
self.local_memory_map = np.zeros((11,11), dtype=np.float32)

# 记忆矩阵的更新函数
def memory_update(self, cur_pos):
"""
记忆矩阵更新
"""
# 全局记忆矩阵
x,z = cur_pos
z = 127 - z
current_value = self.global_memory_map[z, x]
self.global_memory_map[z, x] = min(1.0, current_value + 0.1)

# 局部记忆矩阵
# 计算在全局地图上的源区域边界
src_top = max(0, z - 5)
src_bottom = min(128, z + 6)
src_left = max(0, x - 5)
src_right = min(128, x + 6)

# 计算在局部地图上的目标区域边界
dst_top = src_top - (z - 5)
dst_bottom = src_bottom - (z - 5)
dst_left = src_left - (x - 5)
dst_right = src_right - (x - 5)

# 从全局地图复制有效区域到局部地图
self.local_memory_map[dst_top:dst_bottom, dst_left:dst_right] = self.global_memory_map[src_top:src_bottom, src_left:src_right]
self.memory_flag = self.local_memory_map.flatten()

② 智能体当前的organs状态列表

对于organs状态的信息,我们可以先默认初始化13个随机宝箱的环境,让所有的宝箱都能够在状态里面显示出来,这些在extra_info里面可以详细看到。

我们统一能看到且能收集是状态1、不能看到不能收集是状态0

我们先默认初始化15维度的organs状态列表,代表的是对应15个organ的状态,包括BUFF、宝箱、终点(默认14、方便设计)

# 遍历更新宝箱终点和BUFF视野内状态,0是不可见,1是可见
for target in self.target_states:
if target['type'] != "end":
self.organs_states[target['config_id']] = 1
else:
self.organs_states[14] = 1

上面的设计对于博弈赛道还是具身赛道通用,我这边是做过特殊处理过的,其实默认的organs里面的也可以直接用,根据type去判断是否是宝箱还是终点。

③ 当前智能体所能得到的目标特征

关于这个特征的设计,要区分下不同赛道的设计特点。

对于博弈中级赛道:其实目标是非常明确的,因为obs里面是基本无论是位置信息,还有所有的organ信息基本很全

对于博弈高级赛道:目标的明确性稍差,位置信息不提供,organ信息也基本很全,官方提供了大概的位置计算公式。

对于具身赛道:目标明确性非常差,位置信息提供,organ信息不全,官方默认提供了self.target_pos_list = [(26, 87), (85, 114), (32, 24), (101, 40), (59, 64)] 用于探索,可以作为探索目标进行保留作为特征设计。

对于博弈中级赛道的设计,直接获取对应最短曼哈顿距离的作为目标即可,然后宝箱收集完之后再转目标为终点这个特殊目标。然后默认终点位置不明确可以直接利用官方默认提供的大概估算位置的方法,其他也适用的。

# if end_pos is not found, use relative position to predict end_pos
# 如果终点位置未找到,使用相对位置预测终点位置
elif (not self.is_end_pos_found) and (
self.end_pos is None
or self.cur_step_no % 100 == 0
or self.end_pos_dir != end_pos_dir
or self.end_pos_dis != end_pos_dis
):
# 计算距离
distance = end_pos_dis * 20
# 计算角度
theta = DirectionAngles[end_pos_dir]
# 计算位置偏移
delta_x = distance * math.cos(math.radians(theta))
delta_z = distance * math.sin(math.radians(theta))
# 更新终点位置
self.end_pos = (
max(0, min(128, round(self.cur_pos[0] + delta_x))),
max(0, min(128, round(self.cur_pos[1] + delta_z))),
)

self.end_pos_dir = end_pos_dir
self.end_pos_dis = end_pos_dis

对于博弈高级赛道的设计,位置不明确就大概估算位置,明确就直接用明确的位置信息,一样的和博弈中级赛道取对应最短曼哈顿距离作为目标即可。

对于具身赛达的设计,我们可以先看看能否获取到对应的当前视野的最短距离目标,如果获取不到就转探索目标,然后要时刻记录历史探索到的目标进行存储,方便后面进行优先级的目标切换。在弄个保险的避免目标完全丢失,以自己的位置为目标,不过优先级最低。

对于具身赛道的设计我需要额外说明这个目标的优先级,以及提供多历史目标进入特征,我目前还在尝试,不过目前效果好像还是积极的,我这边分享出来各位可以评判下再试着用用。

具身赛道的目标优先级是这样的

  • 最高优先级:视野内已有的最短目标
  • 高优先级:记忆内的最短宝箱目标
  • 中优先级:探索优先级
  • 特殊优先级(中后面):终点目标,终点目标的话也是需要不断探索才能找到,为什么比探索目标优先级低,是为了防止遗漏宝箱。
  • 低优先级:以自己的位置为目标,”哑“目标。

最终所以赛道得到的目标经过官方提供的目标特征的函数处理完之后加入到特征里面即可

self.feature_target_pos = self._get_pos_feature(self.target['state'] != -1, self.cur_pos, self.target['pos'])   

注:具身赛道和其他赛道其他也可以尝试加入候选目标,所以维度可以是(1+候选目标数量)x6

# 获取新的多目标特征 -- 具身赛道的候选依赖于历史探索到的目标
multi_target_features = self._get_multi_target_features(K=3)

④ 当前位置的one-hot编码特征

这个的话我早期没有加的,因为之前的比赛代码里面是有的,所以我后面也加上了,其实这个特征处理也非常简单,可以参考如下。

# 目标位置的one-hot编码
pos_row = [0] * 128
pos_row[self.cur_pos[0]] = 1
pos_col = [0] * 128
pos_col[self.cur_pos[1]] = 1

所以我最终的特征总维度大概如下

# features
# 特征
FEATURES = [
# 当前位置归一化
2,
# 当前位置的one-hot编码
128,
128,
# 15个物件的状态列表
15,
# 目标的位置特征,(允许加入候选目标)
6 * 3,
# 历史位置特征
6,
# 16个合法动作
8 + 8,
# 5个局部视野域- 宝箱、障碍物、BUFF、终点、记忆矩阵
5*51*51,
]