碎碎念:
老师让我和同学组队参加10月底截止报名的庙算比赛,我俩走运进了64强,打的过程中发现了一个重要问题------为什么别人总能打我,但是我都看不见!就像玩dota被对面英雄莫名其妙单杀了但是他就一直隐身我都不知道怎么死的。还一个就是,步兵在这里非常厉害,在一个地方趴窝除了火炮集火,就像一个魔免英雄,坦克战车来了只能被步兵干死。
这些都源于一张规则表,但是打起来很离谱------环境和距离视野卡对了,这和不会玩的就是两个游戏!
(碎碎念,这个游戏移动过程中都没法更改目标位置,比星际魔兽操控起来死板,而且停止和冷却的cd太长了,75秒和120秒!什么RTS也不会设这么长的冷却时间......只能说这是一个不好玩的游戏)
------2024.12以上
一、神经网络简介
本质:不断求导找拟合,使得loss收敛,使得acc预测准确率变高
用法:编码映射到标签
结构:层级输入输出、loss函数设计、优化器
二、兵棋神经网络设计
参考alphaStar的星际2的监督学习-强化学习的构建道路,同时他们有强大算力来搞大规模的"联盟对抗"(不断的自己对战自己,利用强化学习然后挑选出更好的智能体)
1.明确实现目标
普通人都能叨叨两句"用AI就能实现","人工智能经过训练就可以实现"......这些都是不负责任的随口乱说,我十分讨厌这种空架子(好像当初我本科毕设就是讨厌空架子写的嗅图狗来帮我识图),我们得明确我们的任务是什么!
任务:经过挑选的样本的训练,得到可以准确预测敌方兵力运动方向的神经网络。(这一步还没到可以进行对战的智能体呢!)
2.明确输出、输入,构建数据集
既然输出是预测方位,那就按六边形构建行为空间。
兵棋是六边形的格子,每个格子有6个临边。
定义:0~5,0是正右侧,按逆时针递进,加上原地不同作为6
行为空间是[0,6],即0123456共个行为空间。
于是输出是一个动作向量,长度为7,分别代表7个行动的概率。
有因果关系的输入,我们可以定义各种行为作为输入
比如其他兵力的位置、兵力的血量状态、位置所处的地形......
位置是一个n*m的矩阵,可以作为一个张量输入
血量可以定义为离散的1、2、3、4,作为向量输入
......
最后将这些离散的状态组合到一起,变成一个大张量作为输入
3.数学理论基础
神经网络的数学原理本质上是一个非线性函数逼近器 ,它通过层层线性变换+非线性激活函数来学习数据中的复杂关系。
为什么神经网络能拟合复杂函数?
线性变换+非线性激活 :单独的线性变换无法拟合复杂数据,但非线性激活函数(如 ReLU、Sigmoid)允许网络学到更复杂的映射。
通用逼近定理(Universal Approximation Theorem):只要有足够的隐藏层和神经元,神经网络可以逼近任意连续函数。
完整的神经网络训练过程:
前向传播:计算神经网络输出(经过各个节点的函数得到最后的输出)
计算损失:衡量误差(预测值、真实值之间的差距,评估方式不同loss函数也可以不同)
反向传播:计算梯度( 用链式法则(Chain Rule)传播误差,从输出层向输入层更新权重)
梯度下降更新权重
重复迭代,直到收敛
常见的神经网络的不同架构(Architecture):
MLP(多层感知机):如果输入足够丰富(如历史轨迹、环境状态),MLP 也能拟合出未来轨迹的函数关系。
RNN / LSTM / GRU :这些模型可以记住 过去的信息,从而学会时间序列的依赖关系,对短期轨迹预测效果更好。
CNN(卷积神经网络) :如果数据是空间+时间序列(如战场局势随时间变化的热力图),CNN 也可以提取空间特征并用于未来位置预测。
三、编码及实践
1.onehot编码(独热编码)
对于离散的无关的,使用onehot编码。这里采用的都是没有前后关系的离散数据,于是就用0和1来表示一切的独热编码作为编码。
2.将特征转化为编码
.举一个简单的例子,将json中的占领状态定义成离散的四种独热编码(两个点的,所以是4位长度)
python
# eg. [1, 0, 1, 0]
def get_city_states(stepData):
"""
获取夺控点状态信息
:param stepData: 样本中一帧的数据
:return: 夺控点信息 2个夺控点共4个值 每两位对应一个夺控点, 如果红色方占领则10, 蓝色方01, 未占领00
"""
cities = stepData['cities'] # [{'coord': 3636, 'value': 80, 'flag': -1, 'name': '主要夺控点'}, {'coord': 3739, 'value': 50, 'flag': -1, 'name': '次要夺控点'}]
cities_info = [0] * 4 # [0, 0, 0, 0]
num = 0
for loc in cities:
if loc['flag'] == -1: # 未占领
cities_info[num:num + 2] = [0, 0]
elif loc['flag'] == 0: # 红方占领
cities_info[num:num + 2] = [1, 0]
else: # 蓝方占领
cities_info[num:num + 2] = [0, 1]
num += 2
# print(cities_info) # eg. [1, 0, 1, 0]
return cities_info
将1-1800的连续值,变成5位的离散01独热编码。 (这里展示的是利用np.hstack(np.eye(n)函数创建n阶单位矩阵,np.eye(5)[m]是提取出矩阵中的第m行)
python
cur_stage = np.hstack(np.eye(5)[int(cur_step / 400)]) # eg. [0. 1. 0. 0. 0.]
还一种,选取第m行的单位阵的值,这个方法速度上经试验与上述方法一样高效,更规范不容易出错。
python
def OneHotcode(self,curdata,list):
onehot_encoded = [0]*len(list)
if curdata in list:
onehot_encoded[list.index(curdata)] = 1
else:
onehot_encoded = [-1] * len(list)
return onehot_encoded
OneHotcode(m, [x for x in range(0, 5)])
3. nn.Module使用
3-1模型部分
在 PyTorch 中,所有的神经网络模型都建议继承自 nn.Module,这是因为 nn.Module
提供了许多构建和管理神经网络的基础功能。
python
import torch
import torch.nn as nn
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
# 定义模型的层
self.fc1 = nn.Linear(10, 20) # 全连接层
self.relu = nn.ReLU() # 激活函数
self.fc2 = nn.Linear(20, 1) # 输出层
def forward(self, x):
# 定义前向传播逻辑
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
继承 nn.Module 后,需要在 init 方法中定义模型的层或子模块,并在 forward 方法中定义前向传播逻辑(输入数据x如何穿过这些层训练,又如何把这些层的函数固化让输入穿过后得到期望的输出)。
3-2训练部分
python
# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 模拟训练
for epoch in range(10):
optimizer.zero_grad() # 清空梯度
output = model(input_data) # 前向传播
loss = criterion(output, torch.randn(5, 5)) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新参数
print(f"Epoch {epoch}, Loss: {loss.item()}")
可以通过 model.parameters()
或 model.state_dict()
访问模型的参数。
python
# 查看模型的参数
for name, param in model.named_parameters():
print(name, param.shape)
# 保存模型参数
torch.save(model.state_dict(), "model.pth")
# 加载模型参数
model.load_state_dict(torch.load("model.pth"))
3-3调用部分
创建模型实例后,可以像调用函数一样调用模型,输入数据会自动经过 forward 方法。
python
model = MyModel()
input_data = torch.randn(5, 10) # 假设输入是一个 (5, 10) 的张量
output = model(input_data) # 前向传播
print(output)
4.我暂时的神经网络(仅预测兵力的移动方位)
这部分目前的实现,我考虑多层的决策处理,简单的方位预测我觉得还不够实用。
以后的通用RTS游戏智能体的自定义搭建我要更灵活通用。
python
# 通用兵力策略网络(坦克、战车、步兵)------输出是位置预测
class troopsNN(nn.Module):
"""
输入:游戏状态、兵种特征、空间特征、位置嵌入
"""
def __init__(self):
"""
game_stats: torch.Size([batch_size, 10, 12])
bop_features: torch.Size([batch_size, 10, 12, 44])
spatial_features:torch.Size([batch_size, 10, 14, 92, 77]) -> 送入cnn的形状应该是(b, n, h, w)
bop_embeddings:torch.Size([batch_size, 10, 6, 92, 77])
"""
super(troopsNN, self).__init__()
self.mlp_fg = torch.nn.Linear(12, 6)
self.mlp_fb = torch.nn.Linear(44, 25)
self.cnn_fs = nn.ModuleList()
self.cnn_fs.append(self.conv_layer([17, 24], kernel=3))
self.cnn_fs.append(self.conv_layer([24, 48], kernel=3))
self.down_sampling = nn.MaxPool2d(kernel_size=2, stride=2)
self.mlp_move_direction = self.mlp_layer(7) # TODO: 这里用mask掩码机制,使得不同兵种的动作空间不同
def conv_layer(self, channel, kernel):
conv_block = nn.Sequential(
nn.Conv2d(in_channels=channel[0], out_channels=channel[1], kernel_size=kernel, stride=2, padding=1),
nn.BatchNorm2d(num_features=channel[1]),
nn.ReLU(inplace=True),
)
return conv_block
def mlp_layer(self, out_dim):
mlp_block = nn.Sequential(
nn.Linear(5430, 2048), # notemporal 5430; temporal 54300
nn.ReLU(inplace=True),
nn.Linear(2048, out_dim),
)
return mlp_block
def forward(self, game_stats, bop_features, spatial_features, bop_embeddings):
# 处理特征
game_stats = game_stats.permute(1, 0, 2) # 1, b, 12
bop_features = bop_features.permute(1, 2, 0, 3) # 1, 12, b, 44
spatial_features = spatial_features.permute(1, 0, 2, 3, 4) # 1, b, 23, 92, 77
bop_embeddings = bop_embeddings.permute(1, 0, 2, 3, 4) # 1, b, 3, 92, 77
# final_feature_bop_frame = torch.zeros((6, 10, game_stats.shape[1], 2048), device=device) # (bop, frame, batch_size, length of final feature). too big?
final_feature_frame = ([0] * 1)
for i in range(1):
final_feature_frame[i] = [0] * game_stats.shape[1]
for i in range(1):
for j in range(game_stats.shape[1]):
final_feature_frame[i][j] = [0] * 2048
# print(np.shape(final_feature_frame)) # (1, batch_size, 2048)
for i in range(1):
f_game = self.mlp_fg(game_stats[i]) # input (b, 12), output (b, 6)
for j in range(6): # bops' features (our_info + enemy_info)
fb_j_total = self.mlp_fb(bop_features[i][j]) # in (b, 44), out(b, 25)
fb_j = fb_j_total[:, :-1] # (b, 24)
if j == 0:
f_bops = fb_j
else:
f_bops = torch.cat((f_bops, fb_j), 1) # final f_bops: (b, 24*3=72)
# basic网络中不需要区分算子的位置嵌入,直接把bop_embeddings与空间特征做concat。不再需要从共享特征池做att。
f_spatial = torch.cat((spatial_features[i], bop_embeddings[i]), 1) # (b, 14+3, 92, 77)
for u in range(len(self.cnn_fs)):
f_spatial = self.cnn_fs[u](f_spatial)
f_spatial = self.down_sampling(f_spatial)
f_spatial = f_spatial.view(f_spatial.shape[0], -1) # 拉平成(b, )
final_feature_frame[i] = torch.cat((f_game, f_bops, f_spatial), 1) # (b, )
# print(final_feature_frame[i].shape) # torch.Size([8, 5430])
pred_move_dir = self.mlp_move_direction(final_feature_frame[0])
return pred_move_dir
------就像高中时悟到的那样,自己的笔记是一定要记的,就算别人的笔记再详细,也不是你所经历过的路,也有很多缺失的或多余的,所以不能依赖所谓的学霸笔记,要自己有自己的笔记,自己才能随时取用自己需要的知识。