ns3-gym例子解析_基础例子与wifi例子_DQN(3)

基于我之前的内容,ns3-gym就是起到如下的作用:

ns3可执行程序预埋待触发函数,python触发C++对应状态获取、奖励获取函数、环境获取函数,RL迭代后触发对应的action函数,完成完整的控制,如下图所示:

https://blog.csdn.net/Mr_liu_666/article/details/157653293https://blog.csdn.net/Mr_liu_666/article/details/157653293

在看完了表格式Qlearning的过程之后,我们继续DQN

linear-mesh例子-DQN

ns-3.40/contrib/opengym/examples/linear-mesh

事实上CC的内容和上一次的表格式Qlearning完全一致------为了读者不重新打开网页,我这里再次贴一次cc的分析。

等效分析

现在的训练本质是: 在一个共享无线信道上, 观察单条 UDP 流(node0 → node4)在 DCF 下的时延/丢包/吞吐, 并让 agent 学速率 / 参数选择。

关键特征只有 3 个:

  1. 单一竞争域(single collision domain)

  2. 没有真正的中继转发(逻辑上是一跳)

  3. MAC 行为由 DCF 主导,而非路由/调度

计划修改的结构

Node0 : AP + UdpServer

Node1 : STA

Node2 : STA

Node3 : STA

Node4 : STA + UdpClient

保证:5 个节点在同一 WiFi channel、距离足够近(互相可感知)、无 RTS/CTS(或一致开启)

那么:

AP + 4 STA 在 DCF 下 ≈ 5 个 Adhoc 节点竞争

MAC 层统计特性是等价的,对 Gym 观测量(delay / throughput)是等价的

训练 agent 的反馈是:

  • 包是否成功

  • 接收时延

  • 吞吐变化

计划修改的结构中:

  • AP 不做调度

  • 没有 EDCA 优先级差异

  • 没有 beacon/association 影响业务帧

因此 agent 看到的 因果结构不变。

编译通过的新cc分析

完整源码

cpp 复制代码
/*
 * SPDX-License-Identifier: GPL-2.0-only
 */
#include "ns3/applications-module.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/mobility-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/ssid.h"
#include "ns3/yans-wifi-helper.h"
#include "ns3/gtk-config-store.h"
#include "ns3/ap-wifi-mac.h"
#include "ns3/txop.h"
#include "ns3/qos-txop.h"
#include "ns3/opengym-module.h"
#include "ns3/wifi-module.h"
#include "ns3/spectrum-module.h"
#include "ns3/stats-module.h"
#include "ns3/flow-monitor-module.h"
#include "ns3/traffic-control-module.h"
#include "ns3/node-list.h"
#include "ns3/csma-module.h"
#include <unordered_map>
#include <vector>
#include "ns3/netanim-module.h"

using namespace ns3;
NS_LOG_COMPONENT_DEFINE("TestModule");

void SetCW(NodeContainer wifiApNodes)
{
  Ptr<NetDevice> dev = wifiApNodes.Get(0)->GetDevice(0);
  Ptr<WifiNetDevice> wifi_dev = DynamicCast<WifiNetDevice>(dev);
  Ptr<WifiMac> mac = wifi_dev->GetMac();
  PointerValue ptr;
  Ptr<Txop> txop;
  if (!mac->GetQosSupported())
  {
    mac->GetAttribute("Txop", ptr);
    txop = ptr.Get<Txop>();
    if (txop)
    {
      uint32_t currentMinCw = txop->GetMinCw();
      uint32_t currentMaxCw = txop->GetMaxCw();
      std::cout << "Current CWmin: " << currentMinCw << ", CWmax: " << currentMaxCw << std::endl;

      uint32_t newMinCw = 2;
      uint32_t newMaxCw = 2;
      txop->SetMinCw(newMinCw);
      txop->SetMaxCw(newMaxCw);
      std::cout << "Set CWmin to: " << newMinCw << ", CWmax to: " << newMaxCw << std::endl;
    }
  }
  else
  {
    mac->GetAttribute("VO_Txop", ptr);
    Ptr<QosTxop> vo_txop = ptr.Get<QosTxop>();
    mac->GetAttribute("VI_Txop", ptr);
    Ptr<QosTxop> vi_txop = ptr.Get<QosTxop>();
    mac->GetAttribute("BE_Txop", ptr);
    Ptr<QosTxop> be_txop = ptr.Get<QosTxop>();
    mac->GetAttribute("BK_Txop", ptr);
    Ptr<QosTxop> bk_txop = ptr.Get<QosTxop>();
    uint32_t newMinCw = 2;
    uint32_t newMaxCw = 2;
    be_txop->SetMinCw(newMinCw);
    be_txop->SetMaxCw(newMaxCw);
    vo_txop->SetMinCw(newMinCw);
    vo_txop->SetMaxCw(newMaxCw);
    vi_txop->SetMinCw(newMinCw);
    vi_txop->SetMaxCw(newMaxCw);
    bk_txop->SetMinCw(newMinCw);
    bk_txop->SetMaxCw(newMaxCw);
  }
}

/*
Define observation space
*/
Ptr<OpenGymSpace> MyGetObservationSpace(void)
{
  uint32_t nodeNum = NodeList::GetNNodes();
  float low = 0.0;
  float high = 888888.0;
  std::vector<uint32_t> shape = {
      nodeNum,
  };
  std::string dtype = TypeNameGet<uint32_t>();
  Ptr<OpenGymBoxSpace> space = CreateObject<OpenGymBoxSpace>(low, high, shape, dtype);
  NS_LOG_UNCOND("MyGetObservationSpace: " << space);
  return space;
}
/*
Define action space
*/
Ptr<OpenGymSpace> MyGetActionSpace(void)
{
  uint32_t nodeNum = NodeList::GetNNodes();
  float low = 0.0;
  float high = 666666.0;
  std::vector<uint32_t> shape = {
      nodeNum,
  };
  std::string dtype = TypeNameGet<uint32_t>();
  Ptr<OpenGymBoxSpace> space = CreateObject<OpenGymBoxSpace>(low, high, shape, dtype);
  NS_LOG_UNCOND("MyGetActionSpace: " << space);
  return space;
}
/*
Define game over condition
*/
bool MyGetGameOver(void)
{
  bool isGameOver = false;
  NS_LOG_UNCOND("MyGetGameOver: " << isGameOver);
  return isGameOver;
}
Ptr<WifiMacQueue> GetQueue(Ptr<Node> node)
{
  Ptr<NetDevice> dev = node->GetDevice(0);
  Ptr<WifiNetDevice> wifi_dev = DynamicCast<WifiNetDevice>(dev);
  Ptr<WifiMac> wifi_mac = wifi_dev->GetMac();
  PointerValue ptr;
  wifi_mac->GetAttribute("Txop", ptr);
  Ptr<Txop> txop = ptr.Get<Txop>();

  Ptr<WifiMacQueue> queue;
  if (txop)
  {
    queue = txop->GetWifiMacQueue();
    std::cout << "queue" << queue << std::endl;
  }
  else
  {
    wifi_mac->GetAttribute("BE_Txop", ptr);
    queue = ptr.Get<QosTxop>()->GetWifiMacQueue();
    std::cout << "QosTxop queue" << queue << std::endl;
  }

  return queue;
}
/*
Collect observations
*/
Ptr<OpenGymDataContainer> MyGetObservation(void)
{
  uint32_t nodeNum = NodeList::GetNNodes();
  std::vector<uint32_t> shape = {
      nodeNum,
  };
  Ptr<OpenGymBoxContainer<uint32_t>> box = CreateObject<OpenGymBoxContainer<uint32_t>>(shape);
  for (NodeList::Iterator i = NodeList::Begin(); i != NodeList::End(); ++i)
  {
    Ptr<Node> node = *i;
    Ptr<WifiMacQueue> queue = GetQueue(node);
    uint32_t value = queue->GetNPackets();
    box->AddValue(value);
  }
  NS_LOG_UNCOND("MyGetObservation: " << box);
  return box;
}
uint64_t g_rxPktNum = 0;
void DestRxPkt(std::string context, Ptr<const Packet> packet)
{
  // NS_LOG_UNCOND ("Client received a packet of " << packet->GetSize () << " bytes"<<"No. "<<g_rxPktNum);
  g_rxPktNum++;
}
/*
Define reward function
*/
float MyGetReward(void)
{
  static float lastValue = 0.0;
  float reward = g_rxPktNum - lastValue;
  lastValue = g_rxPktNum;
  NS_LOG_UNCOND("reward: " << reward);
  return reward;
}
/*
Define extra info. Optional
*/
std::string MyGetExtraInfo(void)
{
  std::string myInfo = "linear-wireless-mesh";
  myInfo += "|123";
  NS_LOG_UNCOND("MyGetExtraInfo: " << myInfo);
  return myInfo;
}
bool SetCw(Ptr<Node> node, uint32_t cwMinValue = 0, uint32_t cwMaxValue = 0)
{
  Ptr<NetDevice> dev = node->GetDevice(0);
  Ptr<WifiNetDevice> wifi_dev = DynamicCast<WifiNetDevice>(dev);
  Ptr<WifiMac> wifi_mac = wifi_dev->GetMac();
  PointerValue ptr;
  wifi_mac->GetAttribute("Txop", ptr);
  Ptr<Txop> txop = ptr.Get<Txop>();
  NS_LOG_UNCOND("!!!!!!!!!!!txop: " << txop);
  if (txop)
  {
    uint32_t currentMinCw = txop->GetMinCw();
    uint32_t currentMaxCw = txop->GetMaxCw();
    std::cout << "Current CWmin: " << currentMinCw << ", CWmax: " << currentMaxCw << std::endl;

    txop->SetMinCw(cwMinValue);
    txop->SetMaxCw(cwMaxValue);
    std::cout << "Set CWmin to: " << cwMinValue << ", CWmax to: " << cwMaxValue << std::endl;
  }
  else
  {
    wifi_mac->GetAttribute("VO_Txop", ptr);
    Ptr<QosTxop> vo_txop = ptr.Get<QosTxop>();
    wifi_mac->GetAttribute("VI_Txop", ptr);
    Ptr<QosTxop> vi_txop = ptr.Get<QosTxop>();
    wifi_mac->GetAttribute("BE_Txop", ptr);
    Ptr<QosTxop> be_txop = ptr.Get<QosTxop>();
    wifi_mac->GetAttribute("BK_Txop", ptr);
    Ptr<QosTxop> bk_txop = ptr.Get<QosTxop>();

    uint32_t currentMinCw = be_txop->GetMinCw(0);
    uint32_t currentMaxCw = be_txop->GetMaxCw(0);
    std::cout << "be_txopCurrent CWmin: " << currentMinCw << ", CWmax: " << currentMaxCw << std::endl;

    be_txop->SetMinCw(cwMinValue);
    be_txop->SetMaxCw(cwMaxValue);
    vo_txop->SetMinCw(cwMinValue);
    vo_txop->SetMaxCw(cwMaxValue);
    vi_txop->SetMinCw(cwMinValue);
    vi_txop->SetMaxCw(cwMaxValue);
    bk_txop->SetMinCw(cwMinValue);
    bk_txop->SetMaxCw(cwMaxValue);
    std::cout << "Set CWmin to: " << cwMinValue << ", CWmax to: " << cwMaxValue << std::endl;
  }
  return true;
}
/*
Execute received actions
*/
bool MyExecuteActions(Ptr<OpenGymDataContainer> action)
{
  NS_LOG_UNCOND("MyExecuteActions: " << action);
  Ptr<OpenGymBoxContainer<uint32_t>> box = DynamicCast<OpenGymBoxContainer<uint32_t>>(action);
  std::vector<uint32_t> actionVector = box->GetData();
  uint32_t nodeNum = NodeList::GetNNodes();
  for (uint32_t i = 0; i < nodeNum; i++)
  {
    Ptr<Node> node = NodeList::GetNode(i);
    uint32_t cwSize = actionVector.at(i);
    NS_LOG_UNCOND("i=" << i << "   ;cwSize: " << cwSize);
    SetCw(node, cwSize, cwSize);
  }
  return true;
}

void ScheduleNextStateRead(double envStepTime, Ptr<OpenGymInterface> openGymInterface)
{
  Simulator::Schedule(Seconds(envStepTime), &ScheduleNextStateRead, envStepTime, openGymInterface);
  openGymInterface->NotifyCurrentState();
}

int main(int argc, char *argv[])
{
  bool verbose = true;
  uint32_t nWifi = 4;
  bool tracing = false;
  uint32_t runNumber = 1;
  uint32_t simSeed = 1;
  double simulationTime = 3;
  double envStepTime = 0.1;
  uint32_t openGymPort = 5555;
  uint32_t testArg = 0;

  CommandLine cmd(__FILE__);
  cmd.AddValue("nWifi", "Number of wifi STA devices", nWifi);
  cmd.AddValue("verbose", "Tell echo applications to log if true", verbose);
  cmd.AddValue("tracing", "Enable pcap tracing", tracing);
  cmd.AddValue("runNumber", "Random runNumber", runNumber);
  cmd.AddValue("openGymPort", "Port number for OpenGym env. Default: 5555", openGymPort);
  cmd.AddValue("simSeed", "Seed for random generator. Default: 1", simSeed);
  cmd.AddValue("simTime", "Simulation time in seconds. Default: 10s", simulationTime);
  cmd.AddValue("testArg", "Extra simulation argument. Default: 0", testArg);
  cmd.Parse(argc, argv);

  RngSeedManager::SetSeed(22);
  RngSeedManager::SetRun(simSeed);
  NS_LOG_UNCOND("Ns3Env parameters:");
  NS_LOG_UNCOND("--simulationTime: " << simulationTime);
  NS_LOG_UNCOND("--openGymPort: " << openGymPort);
  NS_LOG_UNCOND("--envStepTime: " << envStepTime);
  NS_LOG_UNCOND("--seed: " << simSeed);
  NS_LOG_UNCOND("--testArg: " << testArg);
  NS_LOG_UNCOND("--runNumber: " << runNumber);

  NodeContainer wifiApNode;
  wifiApNode.Create(1);
  NodeContainer wifiStaNodes;
  wifiStaNodes.Create(nWifi);
  YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
  YansWifiPhyHelper phy;
  phy.SetChannel(channel.Create());
  phy.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO);
  phy.Set("ChannelSettings", StringValue("{36, 20, BAND_5GHZ, 0}"));
  WifiMacHelper mac;
  Ssid ssid = Ssid("ns-3-ssid");
  WifiHelper wifi;
  wifi.SetStandard(WIFI_STANDARD_80211ax);
  wifi.SetRemoteStationManager("ns3::ConstantRateWifiManager",
                               "DataMode", StringValue("HeMcs0"),
                               "ControlMode", StringValue("HeMcs0"));
  wifi.ConfigHeOptions("GuardInterval", TimeValue(NanoSeconds(1600)));
  NetDeviceContainer staDevices;
  mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid), "ActiveProbing", BooleanValue(false), "BE_MaxAmsduSize",
              UintegerValue(0),
              "BE_MaxAmpduSize",
              UintegerValue(0));
  staDevices = wifi.Install(phy, mac, wifiStaNodes);
  NetDeviceContainer apDevices;
  mac.SetType(
      "ns3::ApWifiMac",
      "Ssid",
      SsidValue(ssid),
      "BeaconInterval",
      TimeValue(MicroSeconds(102400)), "BE_MaxAmsduSize",
      UintegerValue(0),
      "BE_MaxAmpduSize",
      UintegerValue(0));
  apDevices = wifi.Install(phy, mac, wifiApNode);
  MobilityHelper mobility;
  mobility.SetPositionAllocator("ns3::GridPositionAllocator",
                                "MinX",
                                DoubleValue(0.0),
                                "MinY",
                                DoubleValue(1.0),
                                "DeltaX",
                                DoubleValue(1.0),
                                "DeltaY",
                                DoubleValue(1.0),
                                "GridWidth",
                                UintegerValue(7),
                                "LayoutType",
                                StringValue("RowFirst"));
  mobility.SetMobilityModel("ns3::RandomWalk2dMobilityModel",
                            "Mode",
                            StringValue("Time"),
                            "Time",
                            StringValue("0.2s"),
                            "Speed",
                            StringValue("ns3::ConstantRandomVariable[Constant=1.0]"),
                            "Bounds",
                            RectangleValue(Rectangle(-500, 500, -500, 500)));

  mobility.Install(wifiStaNodes);
  mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
  mobility.Install(wifiApNode);

  InternetStackHelper stack;
  stack.Install(wifiApNode);
  stack.Install(wifiStaNodes);
  Ipv4AddressHelper address;
  address.SetBase("10.1.1.0", "255.255.255.0");
  Ipv4InterfaceContainer apInterfaces;
  apInterfaces = address.Assign(apDevices);
  Ipv4InterfaceContainer staInterfaces;
  staInterfaces = address.Assign(staDevices);
  UdpServerHelper echoServer(9);
  ApplicationContainer serverApps = echoServer.Install(wifiApNode.Get(0));
  serverApps.Start(Seconds(0));
  serverApps.Stop(Seconds(simulationTime));

  UdpClientHelper Client_node1(apInterfaces.GetAddress(0), 9);
  Client_node1.SetAttribute("MaxPackets", UintegerValue(1000000000));
  Client_node1.SetAttribute("Interval", TimeValue(Seconds(0.0002)));
  Client_node1.SetAttribute("PacketSize", UintegerValue(1000));

  UdpClientHelper Client_other(apInterfaces.GetAddress(0), 9);
  Client_other.SetAttribute("MaxPackets", UintegerValue(1000000000));
  Client_other.SetAttribute("Interval", TimeValue(Seconds(0.0002)));
  Client_other.SetAttribute("PacketSize", UintegerValue(1000));

  ApplicationContainer clientApps;

  clientApps.Add(Client_node1.Install(wifiStaNodes.Get(0)));

  for (uint32_t i = 1; i < wifiStaNodes.GetN(); i++)
  {
    clientApps.Add(Client_other.Install(wifiStaNodes.Get(i)));
  }

  clientApps.Start(Seconds(0));
  clientApps.Stop(Seconds(simulationTime));

  Ipv4GlobalRoutingHelper::PopulateRoutingTables();

  SetCW(wifiApNode);
  for (uint32_t i = 0; i < nWifi; i++)
    SetCW(wifiStaNodes.Get(i));
  Config::Connect("/NodeList/0/ApplicationList/*/$ns3::UdpServer/Rx", MakeCallback(&DestRxPkt));
  if (tracing)
  {
    phy.EnablePcap("third", apDevices.Get(0));
  }

  Ptr<OpenGymInterface> openGymInterface = CreateObject<OpenGymInterface>(openGymPort);
  openGymInterface->SetGetActionSpaceCb(MakeCallback(&MyGetActionSpace));
  openGymInterface->SetGetObservationSpaceCb(MakeCallback(&MyGetObservationSpace));
  openGymInterface->SetGetGameOverCb(MakeCallback(&MyGetGameOver));
  openGymInterface->SetGetObservationCb(MakeCallback(&MyGetObservation));
  openGymInterface->SetGetRewardCb(MakeCallback(&MyGetReward));
  openGymInterface->SetGetExtraInfoCb(MakeCallback(&MyGetExtraInfo));
  openGymInterface->SetExecuteActionsCb(MakeCallback(&MyExecuteActions));
  Simulator::Schedule(Seconds(0.0), &ScheduleNextStateRead, envStepTime, openGymInterface);
  NS_LOG_UNCOND("Simulation start");

  AnimationInterface anim("complex-bridge.xml");
  for (uint32_t i = 1; i < wifiStaNodes.GetN(); i++) {
    anim.UpdateNodeDescription(wifiStaNodes.Get(i), "STA");
    anim.UpdateNodeColor(wifiStaNodes.Get(i), 255, 0, 0);
  }
  anim.UpdateNodeDescription(wifiStaNodes.Get(0), "TargetSTA");
  anim.UpdateNodeColor(wifiStaNodes.Get(0), 255, 255, 0);
  anim.UpdateNodeDescription(wifiApNode.Get(0), "AP");
  anim.UpdateNodeColor(wifiApNode.Get(0), 0, 255, 0);

  Simulator::Stop(Seconds(simulationTime));
  Simulator::Run();
  NS_LOG_UNCOND("Simulation stop");
  openGymInterface->NotifySimulationEnd();
  return 0;
}

Cmakelists文件修改

注意,这里仅仅与NetAnim有关,如果不需要看看STA的随机移动路径,注释掉也没有任何影响。

${libnetanim}是新加入的,ns3中编译可执行程序需要链接对应库,scratch里面在Cmakelists.txt里面已经做了广泛的引用

但对于contrib这里面的可执行文件,需要手动链接一下。

cpp 复制代码
build_lib_example(
  NAME linear-mesh
  SOURCE_FILES linear-mesh/sim.cc
  LIBRARIES_TO_LINK
    ${libapplications}
    ${libcore}
    ${libinternet}
    ${libopengym}
    ${libwifi}
    ${libnetanim}
)

头文件引用与函数声明

cpp 复制代码
#include "ns3/applications-module.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/mobility-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/ssid.h"
#include "ns3/yans-wifi-helper.h"
#include "ns3/ap-wifi-mac.h"
#include "ns3/txop.h"
#include "ns3/qos-txop.h"
#include "ns3/opengym-module.h"
#include "ns3/wifi-module.h"
#include "ns3/spectrum-module.h"
#include "ns3/stats-module.h"
#include "ns3/flow-monitor-module.h"
#include "ns3/traffic-control-module.h"
#include "ns3/node-list.h"
#include "ns3/csma-module.h"
#include <unordered_map>
#include <vector>
#include "ns3/netanim-module.h"

using namespace ns3;
NS_LOG_COMPONENT_DEFINE("TestModule");

void SetCW(NodeContainer wifiApNodes)
{
  Ptr<NetDevice> dev = wifiApNodes.Get(0)->GetDevice(0);
  Ptr<WifiNetDevice> wifi_dev = DynamicCast<WifiNetDevice>(dev);
  Ptr<WifiMac> mac = wifi_dev->GetMac();
  PointerValue ptr;
  Ptr<Txop> txop;
  if (!mac->GetQosSupported())
  {
    mac->GetAttribute("Txop", ptr);
    txop = ptr.Get<Txop>();
    if (txop)
    {
      uint32_t currentMinCw = txop->GetMinCw();
      uint32_t currentMaxCw = txop->GetMaxCw();
      std::cout << "Current CWmin: " << currentMinCw << ", CWmax: " << currentMaxCw << std::endl;

      uint32_t newMinCw = 2;
      uint32_t newMaxCw = 2;
      txop->SetMinCw(newMinCw);
      txop->SetMaxCw(newMaxCw);
      std::cout << "Set CWmin to: " << newMinCw << ", CWmax to: " << newMaxCw << std::endl;
    }
  }
  else
  {
    mac->GetAttribute("VO_Txop", ptr);
    Ptr<QosTxop> vo_txop = ptr.Get<QosTxop>();
    mac->GetAttribute("VI_Txop", ptr);
    Ptr<QosTxop> vi_txop = ptr.Get<QosTxop>();
    mac->GetAttribute("BE_Txop", ptr);
    Ptr<QosTxop> be_txop = ptr.Get<QosTxop>();
    mac->GetAttribute("BK_Txop", ptr);
    Ptr<QosTxop> bk_txop = ptr.Get<QosTxop>();
    uint32_t newMinCw = 2;
    uint32_t newMaxCw = 2;
    be_txop->SetMinCw(newMinCw);
    be_txop->SetMaxCw(newMaxCw);
    vo_txop->SetMinCw(newMinCw);
    vo_txop->SetMaxCw(newMaxCw);
    vi_txop->SetMinCw(newMinCw);
    vi_txop->SetMaxCw(newMaxCw);
    bk_txop->SetMinCw(newMinCw);
    bk_txop->SetMaxCw(newMaxCw);
  }
}

/*
Define observation space
*/
Ptr<OpenGymSpace> MyGetObservationSpace(void)
{
  uint32_t nodeNum = NodeList::GetNNodes();
  float low = 0.0;
  float high = 888888.0;
  std::vector<uint32_t> shape = {
      nodeNum,
  };
  std::string dtype = TypeNameGet<uint32_t>();
  Ptr<OpenGymBoxSpace> space = CreateObject<OpenGymBoxSpace>(low, high, shape, dtype);
  NS_LOG_UNCOND("MyGetObservationSpace: " << space);
  return space;
}
/*
Define action space
*/
Ptr<OpenGymSpace> MyGetActionSpace(void)
{
  uint32_t nodeNum = NodeList::GetNNodes();
  float low = 0.0;
  float high = 666666.0;
  std::vector<uint32_t> shape = {
      nodeNum,
  };
  std::string dtype = TypeNameGet<uint32_t>();
  Ptr<OpenGymBoxSpace> space = CreateObject<OpenGymBoxSpace>(low, high, shape, dtype);
  NS_LOG_UNCOND("MyGetActionSpace: " << space);
  return space;
}
/*
Define game over condition
*/
bool MyGetGameOver(void)
{
  bool isGameOver = false;
  NS_LOG_UNCOND("MyGetGameOver: " << isGameOver);
  return isGameOver;
}
Ptr<WifiMacQueue> GetQueue(Ptr<Node> node)
{
  Ptr<NetDevice> dev = node->GetDevice(0);
  Ptr<WifiNetDevice> wifi_dev = DynamicCast<WifiNetDevice>(dev);
  Ptr<WifiMac> wifi_mac = wifi_dev->GetMac();
  PointerValue ptr;
  wifi_mac->GetAttribute("Txop", ptr);
  Ptr<Txop> txop = ptr.Get<Txop>();

  Ptr<WifiMacQueue> queue;
  if (txop)
  {
    queue = txop->GetWifiMacQueue();
    std::cout << "queue" << queue << std::endl;
  }
  else
  {
    wifi_mac->GetAttribute("BE_Txop", ptr);
    queue = ptr.Get<QosTxop>()->GetWifiMacQueue();
    std::cout << "QosTxop queue" << queue << std::endl;
  }

  return queue;
}
/*
Collect observations
*/
Ptr<OpenGymDataContainer> MyGetObservation(void)
{
  uint32_t nodeNum = NodeList::GetNNodes();
  std::vector<uint32_t> shape = {
      nodeNum,
  };
  Ptr<OpenGymBoxContainer<uint32_t>> box = CreateObject<OpenGymBoxContainer<uint32_t>>(shape);
  for (NodeList::Iterator i = NodeList::Begin(); i != NodeList::End(); ++i)
  {
    Ptr<Node> node = *i;
    Ptr<WifiMacQueue> queue = GetQueue(node);
    uint32_t value = queue->GetNPackets();
    box->AddValue(value);
  }
  NS_LOG_UNCOND("MyGetObservation: " << box);
  return box;
}
uint64_t g_rxPktNum = 0;
void DestRxPkt(std::string context, Ptr<const Packet> packet)
{
  // NS_LOG_UNCOND ("Client received a packet of " << packet->GetSize () << " bytes"<<"No. "<<g_rxPktNum);
  g_rxPktNum++;
}
/*
Define reward function
*/
float MyGetReward(void)
{
  static float lastValue = 0.0;
  float reward = g_rxPktNum - lastValue;
  lastValue = g_rxPktNum;
  NS_LOG_UNCOND("reward: " << reward);
  return reward;
}
/*
Define extra info. Optional
*/
std::string MyGetExtraInfo(void)
{
  std::string myInfo = "linear-wireless-mesh";
  myInfo += "|123";
  NS_LOG_UNCOND("MyGetExtraInfo: " << myInfo);
  return myInfo;
}
bool SetCw(Ptr<Node> node, uint32_t cwMinValue = 0, uint32_t cwMaxValue = 0)
{
  Ptr<NetDevice> dev = node->GetDevice(0);
  Ptr<WifiNetDevice> wifi_dev = DynamicCast<WifiNetDevice>(dev);
  Ptr<WifiMac> wifi_mac = wifi_dev->GetMac();
  PointerValue ptr;
  wifi_mac->GetAttribute("Txop", ptr);
  Ptr<Txop> txop = ptr.Get<Txop>();
  NS_LOG_UNCOND("!!!!!!!!!!!txop: " << txop);
  if (txop)
  {
    uint32_t currentMinCw = txop->GetMinCw();
    uint32_t currentMaxCw = txop->GetMaxCw();
    std::cout << "Current CWmin: " << currentMinCw << ", CWmax: " << currentMaxCw << std::endl;

    txop->SetMinCw(cwMinValue);
    txop->SetMaxCw(cwMaxValue);
    std::cout << "Set CWmin to: " << cwMinValue << ", CWmax to: " << cwMaxValue << std::endl;
  }
  else
  {
    wifi_mac->GetAttribute("VO_Txop", ptr);
    Ptr<QosTxop> vo_txop = ptr.Get<QosTxop>();
    wifi_mac->GetAttribute("VI_Txop", ptr);
    Ptr<QosTxop> vi_txop = ptr.Get<QosTxop>();
    wifi_mac->GetAttribute("BE_Txop", ptr);
    Ptr<QosTxop> be_txop = ptr.Get<QosTxop>();
    wifi_mac->GetAttribute("BK_Txop", ptr);
    Ptr<QosTxop> bk_txop = ptr.Get<QosTxop>();

    uint32_t currentMinCw = be_txop->GetMinCw(0);
    uint32_t currentMaxCw = be_txop->GetMaxCw(0);
    std::cout << "be_txopCurrent CWmin: " << currentMinCw << ", CWmax: " << currentMaxCw << std::endl;

    be_txop->SetMinCw(cwMinValue);
    be_txop->SetMaxCw(cwMaxValue);
    vo_txop->SetMinCw(cwMinValue);
    vo_txop->SetMaxCw(cwMaxValue);
    vi_txop->SetMinCw(cwMinValue);
    vi_txop->SetMaxCw(cwMaxValue);
    bk_txop->SetMinCw(cwMinValue);
    bk_txop->SetMaxCw(cwMaxValue);
    std::cout << "Set CWmin to: " << cwMinValue << ", CWmax to: " << cwMaxValue << std::endl;
  }
  return true;
}
/*
Execute received actions
*/
bool MyExecuteActions(Ptr<OpenGymDataContainer> action)
{
  NS_LOG_UNCOND("MyExecuteActions: " << action);
  Ptr<OpenGymBoxContainer<uint32_t>> box = DynamicCast<OpenGymBoxContainer<uint32_t>>(action);
  std::vector<uint32_t> actionVector = box->GetData();
  uint32_t nodeNum = NodeList::GetNNodes();
  for (uint32_t i = 0; i < nodeNum; i++)
  {
    Ptr<Node> node = NodeList::GetNode(i);
    uint32_t cwSize = actionVector.at(i);
    NS_LOG_UNCOND("i=" << i << "   ;cwSize: " << cwSize);
    SetCw(node, cwSize, cwSize);
  }
  return true;
}

void ScheduleNextStateRead(double envStepTime, Ptr<OpenGymInterface> openGymInterface)
{
  Simulator::Schedule(Seconds(envStepTime), &ScheduleNextStateRead, envStepTime, openGymInterface);
  openGymInterface->NotifyCurrentState();
}
头文件

头文件包含基础h和udp、ipv4、移动性、点对点device、ssid、WiFiphy、WIfiMac、opengym、netanim

全局CW初始化(似乎没啥必要,但保留了)

void SetCW(NodeContainer wifiApNodes),后续调用时初始化为了2,函数目的是使得最开始的动作是2,这样起始的reward更低,现象更明显(但似乎效果一般,因为每个episode有30step,大约两个step,action就把CW改到远大于2的值了)

设置观察空间和动作空间

Ptr<OpenGymSpace> MyGetObservationSpace(void)观察对象是

Ptr<OpenGymSpace> MyGetActionSpace(void)

结束状态配置

bool MyGetGameOver(void)一直返回false,因为本实验没有所谓"游戏失败",因为不管怎么选action,都不会导致传输完全不能进行。

当然可以设置延时太大、吞吐太小、丢包太多作为结束状态,但是我们的例子只是想看到映射关系和训练过程,所以这个还是false,直到一轮ns3仿真结束接口会自动产生true通知python。

状态环境获取

Ptr<OpenGymDataContainer> MyGetObservation(void)调用Ptr<WifiMacQueue> GetQueue(Ptr<Node> node)

MyGetObservation遍历NodeList,拿到每个Node的WiFiMac的queue当前包数目,包越多越说明阻塞,需要响应的减小CW,可以加强竞争、获得更多的接入机会,消耗掉过多的包

GetQueue函数通过DynamicCast获得每一个Node的WiFiMac的queue指针并返回。

奖励获取

DestRxPkt会在UDP收到数据包的时候触发、收到一个包就触发一次,g_rxPktNum就增加一个

MyGetReward通过static float lastValue保存前一个step的收到包数目,每个step的reward就是此step中AP新收到的包数目,通过调节CW,也就是action,最终目的是最大化每个step的收到包数目,也就是奖励,更加细节地,每个step,agent都会根据看到的各个Node的队列长度,尝试调节各个Node的CW,使得AP收到的包最多。

其他信息

此处没有有用的内容,打印类似helloworld的信息"linear-wireless-mesh|123"。

执行动作

MyExecuteActions调用SetCw,MyExecuteActions把动作向量一一实现,比如我有50个STA,action返回的就是一个长度50的类似数组的动作集合,根据各个Node的编号,设置这些Node 的CW值,SetCw通过Node找到WifiMac的指针、获取txop指针、设置对应CW,实现了Action到CW设置的映射。

节拍控制
cpp 复制代码
void ScheduleNextStateRead(double envStepTime, Ptr<OpenGymInterface> openGym)
{
  Simulator::Schedule (Seconds(envStepTime), &ScheduleNextStateRead, envStepTime, openGym);
  openGym->NotifyCurrentState();
}

这部分是最重要的,main函数0.0s触发此函数,此函数每envStepTime时间自我触发一次,被触发时调用的是NotifyCurrentState

cpp 复制代码
void
OpenGymInterface::NotifyCurrentState()
{
  NS_LOG_FUNCTION (this);

  if (!m_initSimMsgSent) {
    Init();
  }

  if (m_stopEnvRequested) {
    return;
  }

  // collect current env state
  Ptr<OpenGymDataContainer> obsDataContainer = GetObservation();
  float reward = GetReward();
  bool isGameOver = IsGameOver();
  std::string extraInfo = GetExtraInfo();

  ns3opengym::EnvStateMsg envStateMsg;
  // observation
  ns3opengym::DataContainer obsDataContainerPbMsg;
  if (obsDataContainer) {
    obsDataContainerPbMsg = obsDataContainer->GetDataContainerPbMsg();
    envStateMsg.mutable_obsdata()->CopyFrom(obsDataContainerPbMsg);
  }
  // reward
  envStateMsg.set_reward(reward);
  // game over
  envStateMsg.set_isgameover(false);
  if (isGameOver)
  {
    envStateMsg.set_isgameover(true);
    if (m_simEnd) {
      envStateMsg.set_reason(ns3opengym::EnvStateMsg::SimulationEnd);
    } else {
      envStateMsg.set_reason(ns3opengym::EnvStateMsg::GameOver);
    }
  }

  // extra info
  envStateMsg.set_info(extraInfo);

  // send env state msg to python
  zmq::message_t request(envStateMsg.ByteSizeLong());;
  envStateMsg.SerializeToArray(request.data(), envStateMsg.ByteSizeLong());
  m_zmq_socket.send (request, zmq::send_flags::none);

  // receive act msg form python
  ns3opengym::EnvActMsg envActMsg;
  zmq::message_t reply;
  (void) m_zmq_socket.recv (reply, zmq::recv_flags::none);
  envActMsg.ParseFromArray(reply.data(), reply.size());

  if (m_simEnd) {
    // if sim end only rx ms and quit
    return;
  }

  bool stopSim = envActMsg.stopsimreq();
  if (stopSim) {
    NS_LOG_DEBUG("---Stop requested: " << stopSim);
    m_stopEnvRequested = true;
    Simulator::Stop();
    Simulator::Destroy ();
    std::exit(0);
  }

  // first step after reset is called without actions, just to get current state
  ns3opengym::DataContainer actDataContainerPbMsg = envActMsg.actdata();
  Ptr<OpenGymDataContainer> actDataContainer = OpenGymDataContainer::CreateFromDataContainerPbMsg(actDataContainerPbMsg);
  ExecuteActions(actDataContainer);

}

其中m_zmq_socket.recv函数是阻塞的,也就是0.0s时,ns3就开始等python的socket消息了,等不到就不往下走了------每次python step就会发一次python的socket消息,也就是python step一次这边ns3就往前走envStepTime,然后再重新开始等socket消息。

主函数与Wifi环境配置

cpp 复制代码
int main(int argc, char *argv[])
{
  bool verbose = true;
  uint32_t nWifi = 4;
  bool tracing = false;
  uint32_t runNumber = 1;
  uint32_t simSeed = 1;
  double simulationTime = 3;
  double envStepTime = 0.1;
  uint32_t openGymPort = 5555;
  uint32_t testArg = 0;

  CommandLine cmd(__FILE__);
  cmd.AddValue("nWifi", "Number of wifi STA devices", nWifi);
  cmd.AddValue("verbose", "Tell echo applications to log if true", verbose);
  cmd.AddValue("tracing", "Enable pcap tracing", tracing);
  cmd.AddValue("runNumber", "Random runNumber", runNumber);
  cmd.AddValue("openGymPort", "Port number for OpenGym env. Default: 5555", openGymPort);
  cmd.AddValue("simSeed", "Seed for random generator. Default: 1", simSeed);
  cmd.AddValue("simTime", "Simulation time in seconds. Default: 10s", simulationTime);
  cmd.AddValue("testArg", "Extra simulation argument. Default: 0", testArg);
  cmd.Parse(argc, argv);

  RngSeedManager::SetSeed(22);
  RngSeedManager::SetRun(simSeed);
  NS_LOG_UNCOND("Ns3Env parameters:");
  NS_LOG_UNCOND("--simulationTime: " << simulationTime);
  NS_LOG_UNCOND("--openGymPort: " << openGymPort);
  NS_LOG_UNCOND("--envStepTime: " << envStepTime);
  NS_LOG_UNCOND("--seed: " << simSeed);
  NS_LOG_UNCOND("--testArg: " << testArg);
  NS_LOG_UNCOND("--runNumber: " << runNumber);

  NodeContainer wifiApNode;
  wifiApNode.Create(1);
  NodeContainer wifiStaNodes;
  wifiStaNodes.Create(nWifi);
  YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
  YansWifiPhyHelper phy;
  phy.SetChannel(channel.Create());
  phy.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO);
  phy.Set("ChannelSettings", StringValue("{36, 20, BAND_5GHZ, 0}"));
  WifiMacHelper mac;
  Ssid ssid = Ssid("ns-3-ssid");
  WifiHelper wifi;
  wifi.SetStandard(WIFI_STANDARD_80211ax);
  wifi.SetRemoteStationManager("ns3::ConstantRateWifiManager",
                               "DataMode", StringValue("HeMcs0"),
                               "ControlMode", StringValue("HeMcs0"));
  wifi.ConfigHeOptions("GuardInterval", TimeValue(NanoSeconds(1600)));
  NetDeviceContainer staDevices;
  mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid), "ActiveProbing", BooleanValue(false), "BE_MaxAmsduSize",
              UintegerValue(0),
              "BE_MaxAmpduSize",
              UintegerValue(0));
  staDevices = wifi.Install(phy, mac, wifiStaNodes);
  NetDeviceContainer apDevices;
  mac.SetType(
      "ns3::ApWifiMac",
      "Ssid",
      SsidValue(ssid),
      "BeaconInterval",
      TimeValue(MicroSeconds(102400)), "BE_MaxAmsduSize",
      UintegerValue(0),
      "BE_MaxAmpduSize",
      UintegerValue(0));
  apDevices = wifi.Install(phy, mac, wifiApNode);
  MobilityHelper mobility;
  mobility.SetPositionAllocator("ns3::GridPositionAllocator",
                                "MinX",
                                DoubleValue(0.0),
                                "MinY",
                                DoubleValue(1.0),
                                "DeltaX",
                                DoubleValue(1.0),
                                "DeltaY",
                                DoubleValue(1.0),
                                "GridWidth",
                                UintegerValue(7),
                                "LayoutType",
                                StringValue("RowFirst"));
  mobility.SetMobilityModel("ns3::RandomWalk2dMobilityModel",
                            "Mode",
                            StringValue("Time"),
                            "Time",
                            StringValue("0.2s"),
                            "Speed",
                            StringValue("ns3::ConstantRandomVariable[Constant=1.0]"),
                            "Bounds",
                            RectangleValue(Rectangle(-500, 500, -500, 500)));

  mobility.Install(wifiStaNodes);
  mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
  mobility.Install(wifiApNode);

  InternetStackHelper stack;
  stack.Install(wifiApNode);
  stack.Install(wifiStaNodes);
  Ipv4AddressHelper address;
  address.SetBase("10.1.1.0", "255.255.255.0");
  Ipv4InterfaceContainer apInterfaces;
  apInterfaces = address.Assign(apDevices);
  Ipv4InterfaceContainer staInterfaces;
  staInterfaces = address.Assign(staDevices);
  UdpServerHelper echoServer(9);
  ApplicationContainer serverApps = echoServer.Install(wifiApNode.Get(0));
  serverApps.Start(Seconds(0));
  serverApps.Stop(Seconds(simulationTime));

  UdpClientHelper Client_node1(apInterfaces.GetAddress(0), 9);
  Client_node1.SetAttribute("MaxPackets", UintegerValue(1000000000));
  Client_node1.SetAttribute("Interval", TimeValue(Seconds(0.0002)));
  Client_node1.SetAttribute("PacketSize", UintegerValue(1000));

  UdpClientHelper Client_other(apInterfaces.GetAddress(0), 9);
  Client_other.SetAttribute("MaxPackets", UintegerValue(1000000000));
  Client_other.SetAttribute("Interval", TimeValue(Seconds(0.0002)));
  Client_other.SetAttribute("PacketSize", UintegerValue(1000));

  ApplicationContainer clientApps;

  clientApps.Add(Client_node1.Install(wifiStaNodes.Get(0)));

  for (uint32_t i = 1; i < wifiStaNodes.GetN(); i++)
  {
    clientApps.Add(Client_other.Install(wifiStaNodes.Get(i)));
  }

  clientApps.Start(Seconds(0));
  clientApps.Stop(Seconds(simulationTime));

  Ipv4GlobalRoutingHelper::PopulateRoutingTables();

  SetCW(wifiApNode);
  for (uint32_t i = 0; i < nWifi; i++)
    SetCW(wifiStaNodes.Get(i));
  Config::Connect("/NodeList/0/ApplicationList/*/$ns3::UdpServer/Rx", MakeCallback(&DestRxPkt));
  if (tracing)
  {
    phy.EnablePcap("third", apDevices.Get(0));
  }

  Ptr<OpenGymInterface> openGymInterface = CreateObject<OpenGymInterface>(openGymPort);
  openGymInterface->SetGetActionSpaceCb(MakeCallback(&MyGetActionSpace));
  openGymInterface->SetGetObservationSpaceCb(MakeCallback(&MyGetObservationSpace));
  openGymInterface->SetGetGameOverCb(MakeCallback(&MyGetGameOver));
  openGymInterface->SetGetObservationCb(MakeCallback(&MyGetObservation));
  openGymInterface->SetGetRewardCb(MakeCallback(&MyGetReward));
  openGymInterface->SetGetExtraInfoCb(MakeCallback(&MyGetExtraInfo));
  openGymInterface->SetExecuteActionsCb(MakeCallback(&MyExecuteActions));
  Simulator::Schedule(Seconds(0.0), &ScheduleNextStateRead, envStepTime, openGymInterface);
  NS_LOG_UNCOND("Simulation start");

  AnimationInterface anim("complex-bridge.xml");
  for (uint32_t i = 1; i < wifiStaNodes.GetN(); i++) {
    anim.UpdateNodeDescription(wifiStaNodes.Get(i), "STA");
    anim.UpdateNodeColor(wifiStaNodes.Get(i), 255, 0, 0);
  }
  anim.UpdateNodeDescription(wifiStaNodes.Get(0), "TargetSTA");
  anim.UpdateNodeColor(wifiStaNodes.Get(0), 255, 255, 0);
  anim.UpdateNodeDescription(wifiApNode.Get(0), "AP");
  anim.UpdateNodeColor(wifiApNode.Get(0), 0, 255, 0);

  Simulator::Stop(Seconds(simulationTime));
  Simulator::Run();
  NS_LOG_UNCOND("Simulation stop");
  openGymInterface->NotifySimulationEnd();
  return 0;
}
ns3全局变量与外部控制接口设置

全局变量包括STA数目nWifi、是否存pcap tracing、当前随机数种子基础上计算随机数的键值simSeed、仿真持续时间simulationTime、单step时间envStepTime(simulationTime/envStepTime就是一个episode对应的step数目)、受python控的zmp的tcp端口openGymPort。

其中nWifi、verbose、tracing、runNumber、openGymPort、simSeed、simTime、testArg是可以ns3 run "linear-mesh --xxxxxx=yyyyy"传递进来的。

然后NS_LOG_UNCOND打印对应值信息。

注意envStepTime写死了,python那边也写死了,没有改而已。

cpp 复制代码
int main(int argc, char *argv[])
{
  bool verbose = true;
  uint32_t nWifi = 4;
  bool tracing = false;
  uint32_t runNumber = 1;
  uint32_t simSeed = 1;
  double simulationTime = 3;
  double envStepTime = 0.1;
  uint32_t openGymPort = 5555;
  uint32_t testArg = 0;

  CommandLine cmd(__FILE__);
  cmd.AddValue("nWifi", "Number of wifi STA devices", nWifi);
  cmd.AddValue("verbose", "Tell echo applications to log if true", verbose);
  cmd.AddValue("tracing", "Enable pcap tracing", tracing);
  cmd.AddValue("runNumber", "Random runNumber", runNumber);
  cmd.AddValue("openGymPort", "Port number for OpenGym env. Default: 5555", openGymPort);
  cmd.AddValue("simSeed", "Seed for random generator. Default: 1", simSeed);
  cmd.AddValue("simTime", "Simulation time in seconds. Default: 10s", simulationTime);
  cmd.AddValue("testArg", "Extra simulation argument. Default: 0", testArg);
  cmd.Parse(argc, argv);

  RngSeedManager::SetSeed(22);
  RngSeedManager::SetRun(simSeed);
  NS_LOG_UNCOND("Ns3Env parameters:");
  NS_LOG_UNCOND("--simulationTime: " << simulationTime);
  NS_LOG_UNCOND("--openGymPort: " << openGymPort);
  NS_LOG_UNCOND("--envStepTime: " << envStepTime);
  NS_LOG_UNCOND("--seed: " << simSeed);
  NS_LOG_UNCOND("--testArg: " << testArg);
  NS_LOG_UNCOND("--runNumber: " << runNumber);
测试环境设置

创建了1个ApNode和nWifi个StaNodes,

信道选用YansWifiChannel,

phy选择的是"{36, 20, BAND_5GHZ, 0}"配置,

ssid设置为"ns-3-ssid",

协议是80211ax,

数据速率是HeMcs0控制帧是HeMcs0(目的是使得信道占用最严重,多竞争,更容易导致错误的CW选择导致坏的状态,此时RL对于CW的优化看起来效果更好),

gi选择1600ns,

关闭聚合(事实上开聚合碰撞更严重,前提是关掉RTS CTS,但是当我开了聚合之后,50STA碰撞太严重,几乎聚不起来,frame全miss了,所以我们看关闭聚合),

移动性采取了一行7个,间隔1的分布,后续在正负500的范围内,速度1m/s,每0.2s变方向动一下,AP不动,STA随机动

AP STA都是10.1.1.X网段

wifiApNode是UDP接收,所有的StaNodes都会UDP发送,端口9,包长1000byte,0.0002s一个包(也就是相当饱和)

UDP应用0s开始,simulationTime时结束,也就是一直跑

初始化全局CW为2,函数目的是使得最开始的动作是2,这样起始的reward更低,现象更明显(但似乎效果一般,因为每个episode有30step,大约两个step,action就把CW改到远大于2的值了)

保存pcap为third

cpp 复制代码
NodeContainer wifiApNode;
  wifiApNode.Create(1);
  NodeContainer wifiStaNodes;
  wifiStaNodes.Create(nWifi);
  YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
  YansWifiPhyHelper phy;
  phy.SetChannel(channel.Create());
  phy.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO);
  phy.Set("ChannelSettings", StringValue("{36, 20, BAND_5GHZ, 0}"));
  WifiMacHelper mac;
  Ssid ssid = Ssid("ns-3-ssid");
  WifiHelper wifi;
  wifi.SetStandard(WIFI_STANDARD_80211ax);
  wifi.SetRemoteStationManager("ns3::ConstantRateWifiManager",
                               "DataMode", StringValue("HeMcs0"),
                               "ControlMode", StringValue("HeMcs0"));
  wifi.ConfigHeOptions("GuardInterval", TimeValue(NanoSeconds(1600)));
  NetDeviceContainer staDevices;
  mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid), "ActiveProbing", BooleanValue(false), "BE_MaxAmsduSize",
              UintegerValue(0),
              "BE_MaxAmpduSize",
              UintegerValue(0));
  staDevices = wifi.Install(phy, mac, wifiStaNodes);
  NetDeviceContainer apDevices;
  mac.SetType(
      "ns3::ApWifiMac",
      "Ssid",
      SsidValue(ssid),
      "BeaconInterval",
      TimeValue(MicroSeconds(102400)), "BE_MaxAmsduSize",
      UintegerValue(0),
      "BE_MaxAmpduSize",
      UintegerValue(0));
  apDevices = wifi.Install(phy, mac, wifiApNode);
  MobilityHelper mobility;
  mobility.SetPositionAllocator("ns3::GridPositionAllocator",
                                "MinX",
                                DoubleValue(0.0),
                                "MinY",
                                DoubleValue(1.0),
                                "DeltaX",
                                DoubleValue(1.0),
                                "DeltaY",
                                DoubleValue(1.0),
                                "GridWidth",
                                UintegerValue(7),
                                "LayoutType",
                                StringValue("RowFirst"));
  mobility.SetMobilityModel("ns3::RandomWalk2dMobilityModel",
                            "Mode",
                            StringValue("Time"),
                            "Time",
                            StringValue("0.2s"),
                            "Speed",
                            StringValue("ns3::ConstantRandomVariable[Constant=1.0]"),
                            "Bounds",
                            RectangleValue(Rectangle(-500, 500, -500, 500)));

  mobility.Install(wifiStaNodes);
  mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
  mobility.Install(wifiApNode);

  InternetStackHelper stack;
  stack.Install(wifiApNode);
  stack.Install(wifiStaNodes);
  Ipv4AddressHelper address;
  address.SetBase("10.1.1.0", "255.255.255.0");
  Ipv4InterfaceContainer apInterfaces;
  apInterfaces = address.Assign(apDevices);
  Ipv4InterfaceContainer staInterfaces;
  staInterfaces = address.Assign(staDevices);
  UdpServerHelper echoServer(9);
  ApplicationContainer serverApps = echoServer.Install(wifiApNode.Get(0));
  serverApps.Start(Seconds(0));
  serverApps.Stop(Seconds(simulationTime));

  UdpClientHelper Client_node1(apInterfaces.GetAddress(0), 9);
  Client_node1.SetAttribute("MaxPackets", UintegerValue(1000000000));
  Client_node1.SetAttribute("Interval", TimeValue(Seconds(0.0002)));
  Client_node1.SetAttribute("PacketSize", UintegerValue(1000));

  UdpClientHelper Client_other(apInterfaces.GetAddress(0), 9);
  Client_other.SetAttribute("MaxPackets", UintegerValue(1000000000));
  Client_other.SetAttribute("Interval", TimeValue(Seconds(0.0002)));
  Client_other.SetAttribute("PacketSize", UintegerValue(1000));

  ApplicationContainer clientApps;

  clientApps.Add(Client_node1.Install(wifiStaNodes.Get(0)));

  for (uint32_t i = 1; i < wifiStaNodes.GetN(); i++)
  {
    clientApps.Add(Client_other.Install(wifiStaNodes.Get(i)));
  }

  clientApps.Start(Seconds(0));
  clientApps.Stop(Seconds(simulationTime));

  Ipv4GlobalRoutingHelper::PopulateRoutingTables();

  SetCW(wifiApNode);
  for (uint32_t i = 0; i < nWifi; i++)
    SetCW(wifiStaNodes.Get(i));
  
  if (tracing)
  {
    phy.EnablePcap("third", apDevices.Get(0));
  }
ns3-gym函数映射

APNode收到Udp后会触发DestRxPkt,记录到全局变量g_rxPktNum,作为reward传递到python侧

MyGetActionSpace提供python和ActionSpace的绑定

MyGetObservationSpace提供python和ObservationSpace的绑定

MyGetGameOver提供python和结束条件的绑定

MyGetObservation提供python和Observation结果的绑定

MyGetReward提供python和奖励的绑定

MyGetExtraInfo提供python和其他信息的绑定

MyExecuteActions提供python和Action到CW控制的绑定

ScheduleNextStateRead是节拍器,Simulator::Schedule(Seconds(0.0)负责启动节拍器。

cpp 复制代码
Config::Connect("/NodeList/0/ApplicationList/*/$ns3::UdpServer/Rx", MakeCallback(&DestRxPkt));

  Ptr<OpenGymInterface> openGymInterface = CreateObject<OpenGymInterface>(openGymPort);
  openGymInterface->SetGetActionSpaceCb(MakeCallback(&MyGetActionSpace));
  openGymInterface->SetGetObservationSpaceCb(MakeCallback(&MyGetObservationSpace));
  openGymInterface->SetGetGameOverCb(MakeCallback(&MyGetGameOver));
  openGymInterface->SetGetObservationCb(MakeCallback(&MyGetObservation));
  openGymInterface->SetGetRewardCb(MakeCallback(&MyGetReward));
  openGymInterface->SetGetExtraInfoCb(MakeCallback(&MyGetExtraInfo));
  openGymInterface->SetExecuteActionsCb(MakeCallback(&MyExecuteActions));
  Simulator::Schedule(Seconds(0.0), &ScheduleNextStateRead, envStepTime, openGymInterface);
  NS_LOG_UNCOND("Simulation start");

路径记录与结束通知

使用NetAnim记录所有Node的路径,标记了STA和AP,Target是后期其他训练需要单独针对某一个STA的时候单独拎出来的时候的一个预留。

NotifySimulationEnd就是在ns3 simulation stop的时候主动gameover的动作,后面具体看下。

cpp 复制代码
 AnimationInterface anim("complex-bridge.xml");
  for (uint32_t i = 1; i < wifiStaNodes.GetN(); i++) {
    anim.UpdateNodeDescription(wifiStaNodes.Get(i), "STA");
    anim.UpdateNodeColor(wifiStaNodes.Get(i), 255, 0, 0);
  }
  anim.UpdateNodeDescription(wifiStaNodes.Get(0), "TargetSTA");
  anim.UpdateNodeColor(wifiStaNodes.Get(0), 255, 255, 0);
  anim.UpdateNodeDescription(wifiApNode.Get(0), "AP");
  anim.UpdateNodeColor(wifiApNode.Get(0), 0, 255, 0);

  Simulator::Stop(Seconds(simulationTime));
  Simulator::Run();
  NS_LOG_UNCOND("Simulation stop");
  openGymInterface->NotifySimulationEnd();
  return 0;
}

NotifySimulationEnd先把全局变量m_simEnd配置成true,然后调用WaitForStop,然后调用NotifyCurrentState,然后调用IsGameOver,得到返回值"return (gameOver || m_simEnd);"后面告诉python的message就是需要over了。

cpp 复制代码
bool
OpenGymInterface::IsGameOver()
{
  NS_LOG_FUNCTION (this);
  bool gameOver = false;
  if (!m_gameOverCb.IsNull())
  {
    gameOver = m_gameOverCb();
  }
  return (gameOver || m_simEnd);
}

void
OpenGymInterface::NotifyCurrentState()
{
  NS_LOG_FUNCTION (this);

  if (!m_initSimMsgSent) {
    Init();
  }

  if (m_stopEnvRequested) {
    return;
  }

  // collect current env state
  Ptr<OpenGymDataContainer> obsDataContainer = GetObservation();
  float reward = GetReward();
  bool isGameOver = IsGameOver();
  std::string extraInfo = GetExtraInfo();

  ns3opengym::EnvStateMsg envStateMsg;
  // observation
  ns3opengym::DataContainer obsDataContainerPbMsg;
  if (obsDataContainer) {
    obsDataContainerPbMsg = obsDataContainer->GetDataContainerPbMsg();
    envStateMsg.mutable_obsdata()->CopyFrom(obsDataContainerPbMsg);
  }
  // reward
  envStateMsg.set_reward(reward);
  // game over
  envStateMsg.set_isgameover(false);
  if (isGameOver)
  {
    envStateMsg.set_isgameover(true);
    if (m_simEnd) {
      envStateMsg.set_reason(ns3opengym::EnvStateMsg::SimulationEnd);
    } else {
      envStateMsg.set_reason(ns3opengym::EnvStateMsg::GameOver);
    }
  }

  // extra info
  envStateMsg.set_info(extraInfo);

  // send env state msg to python
  zmq::message_t request(envStateMsg.ByteSizeLong());;
  envStateMsg.SerializeToArray(request.data(), envStateMsg.ByteSizeLong());
  m_zmq_socket.send (request, zmq::send_flags::none);

  // receive act msg form python
  ns3opengym::EnvActMsg envActMsg;
  zmq::message_t reply;
  (void) m_zmq_socket.recv (reply, zmq::recv_flags::none);
  envActMsg.ParseFromArray(reply.data(), reply.size());

  if (m_simEnd) {
    // if sim end only rx ms and quit
    return;
  }

  bool stopSim = envActMsg.stopsimreq();
  if (stopSim) {
    NS_LOG_DEBUG("---Stop requested: " << stopSim);
    m_stopEnvRequested = true;
    Simulator::Stop();
    Simulator::Destroy ();
    std::exit(0);
  }

  // first step after reset is called without actions, just to get current state
  ns3opengym::DataContainer actDataContainerPbMsg = envActMsg.actdata();
  Ptr<OpenGymDataContainer> actDataContainer = OpenGymDataContainer::CreateFromDataContainerPbMsg(actDataContainerPbMsg);
  ExecuteActions(actDataContainer);

}

void
OpenGymInterface::WaitForStop()
{
  NS_LOG_FUNCTION (this);
  NS_LOG_UNCOND("Wait for stop message");
  NotifyCurrentState();
}

void
OpenGymInterface::NotifySimulationEnd()
{
  NS_LOG_FUNCTION (this);
  m_simEnd = true;
  if (m_initSimMsgSent) {
    WaitForStop();
  }
}

对应python分析

现在看dqn-agent-v1.py和dqn-agent-v2.py以下是dqn-agent-v1.py,对于v2只是inputQueues不同,也就是说输入到get_action的状态维度不同而已,v1采取的是相邻两个node的缓冲区的包,数目,让全链接网络自己学习,而v2采取的是相邻两个node的缓冲区的包的包值的差值,让全链接网络自己学习,所以我们只看一个v1就行了。

cpp 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import scipy.io as io
import gym
import tensorflow as tf
import tensorflow.contrib.slim as slim
import numpy as np
import matplotlib.pyplot as plt
from tensorflow import keras
from ns3gym import ns3env


class DqnAgent(object):
    """docstring for DqnAgent"""
    def __init__(self, inNum, outNum):
        super(DqnAgent, self).__init__()
        self.model = keras.Sequential()
        self.model.add(keras.layers.Dense(inNum, input_shape=(inNum,), activation='relu'))
        self.model.add(keras.layers.Dense(outNum, activation='softmax'))
        self.model.compile(optimizer=tf.train.AdamOptimizer(0.001),
                           loss='categorical_crossentropy',
                           metrics=['accuracy'])

    def get_action(self, state):
        return np.argmax(self.model.predict(state)[0])

    def predict(self, next_state):
        return self.model.predict(next_state)[0]

    def fit(self, state, target, action):
        target_f = self.model.predict(state)
        target_f[0][action] = target
        self.model.fit(state, target_f, epochs=1, verbose=0)


# Environment initialization
port = 5561
simTime = 10 # seconds
startSim = True
stepTime = 0.05 # seconds
seed = 132
simArgs = {"--simTime": simTime,
           "--testArg": 123,
           "--nodeNum": 5,
           "--distance": 500}
debug = False

env = ns3env.Ns3Env(port=port, stepTime=stepTime, startSim=startSim, simSeed=seed, simArgs=simArgs, debug=debug)
#env = gym.make('ns3-v0')

ob_space = env.observation_space
ac_space = env.action_space
print("Observation space: ", ob_space,  ob_space.dtype)
print("Action space: ", ac_space, ac_space.dtype)
s_size = ob_space.shape[0]
a_size = ac_space.shape[0]

inputQueues = 2
cwSize = 100

agent0 = DqnAgent(inputQueues, cwSize)
agent1 = DqnAgent(inputQueues, cwSize)
agent2 = DqnAgent(inputQueues, cwSize)
agent3 = DqnAgent(inputQueues, cwSize)

total_episodes = 50
max_env_steps = 100
env._max_episode_steps = max_env_steps

epsilon = 1.0               # exploration rate
epsilon_min = 0.01
epsilon_decay = 0.999

time_history = []
rew_history = []

for e in range(total_episodes):

    state = env.reset()
    state = np.reshape(state, [1, s_size])
    rewardsum = 0
    for time in range(max_env_steps):

        # Choose action
        if np.random.rand(1) < epsilon:
            action0 = np.random.randint(cwSize)
            action1 = np.random.randint(cwSize)
            action2 = np.random.randint(cwSize)
            action3 = np.random.randint(cwSize)
        else:
            action0 = agent0.get_action(state[:,0:2])
            action1 = agent1.get_action(state[:,1:3])
            action2 = agent2.get_action(state[:,2:4])
            action3 = agent3.get_action(state[:,3:5])

        # Step
        actionVec = [action0, action1, action2, action3, 100]
        next_state, reward, done, _ = env.step(actionVec)

        if done:
            print("episode: {}/{}, time: {}, rew: {}, eps: {:.2}"
                  .format(e, total_episodes, time, rewardsum, epsilon))
            break

        next_state = np.reshape(next_state, [1, s_size])

        # Train
        target0 = reward
        target1 = reward
        target2 = reward
        target3 = reward

        if not done:
            target0 = reward + 0.95 * np.amax(agent0.predict(next_state[:,0:2]))
            target1 = reward + 0.95 * np.amax(agent1.predict(next_state[:,1:3]))
            target2 = reward + 0.95 * np.amax(agent2.predict(next_state[:,2:4]))
            target3 = reward + 0.95 * np.amax(agent3.predict(next_state[:,3:5]))

        agent0.fit(state[:,0:2], target0, action0)
        agent1.fit(state[:,1:3], target1, action1)
        agent2.fit(state[:,2:4], target2, action2)
        agent3.fit(state[:,3:5], target3, action3)

        state = next_state
        rewardsum += reward
        if epsilon > epsilon_min: epsilon *= epsilon_decay
        
    time_history.append(time)
    rew_history.append(rewardsum)

#for n in range(2 ** s_size):
#    state = [n >> i & 1 for i in range(0, 2)]
#    state = np.reshape(state, [1, s_size])
#    print("state " + str(state) 
#        + " -> prediction " + str(model.predict(state)[0])
#        )

#print(model.get_config())
#print(model.to_json())
#print(model.get_weights())

plt.plot(range(len(time_history)), time_history)
plt.plot(range(len(rew_history)), rew_history)
plt.xlabel('Episode')
plt.ylabel('Time')
plt.show()


curve0 = np.zeros(shape=(101,101))
curve1 = np.zeros(shape=(101,101))
curve2 = np.zeros(shape=(101,101))
curve3 = np.zeros(shape=(101,101))

for i in range(101):
    for j in range(101):
        state = np.array([i,j])
        state = np.reshape(state, [1, 2])

        curve0[i,j] = agent0.get_action(state)
        curve1[i,j] = agent1.get_action(state)
        curve2[i,j] = agent2.get_action(state)
        curve3[i,j] = agent3.get_action(state)
        
print("Save curves to MATLAB file")
io.savemat("curves_2d.mat", {
                '0':curve0,
                '1':curve1,
                '2':curve2,
                '3':curve3,
               }
          )

每句话都有注释的代码

cpp 复制代码
#!/usr/bin/env python3
# 指定脚本解释器为 python3(Unix 环境下的 shebang 行)

# -*- coding: utf-8 -*-
# 声明源文件编码为 UTF-8,以支持中文注释和字符串

import scipy.io as io
# 导入 scipy.io 模块并命名为 io,用于保存和加载 MATLAB 格式文件(.mat)

import gym
# 导入 OpenAI Gym 库,提供强化学习环境的标准接口(尽管本代码未直接使用 gym 创建环境)

import tensorflow as tf
# 导入 TensorFlow 深度学习框架

import tensorflow.contrib.slim as slim
# 导入 TensorFlow 的 contrib.slim 模块(用于简化网络定义,但本代码未实际使用)

import numpy as np
# 导入 NumPy 库,用于数值计算和数组操作

import matplotlib.pyplot as plt
# 导入 Matplotlib 的 pyplot 模块,用于绘图

from tensorflow import keras
# 从 TensorFlow 中导入 Keras API,用于构建和训练神经网络模型

from ns3gym import ns3env
# 从 ns3gym 包中导入 ns3env 模块,用于创建与 ns-3 仿真环境交互的 Gym 环境

class DqnAgent(object):
    """docstring for DqnAgent"""
    # 定义一个 DQN 智能体类,继承自 object(Python 3 中所有类默认继承 object)
    # 类的文档字符串(docstring)

    def __init__(self, inNum, outNum):
        # 类的构造函数,用于初始化 DQN 智能体
        # 参数 inNum:输入维度(状态特征数)
        # 参数 outNum:输出维度(动作数)

        super(DqnAgent, self).__init__()
        # 调用父类 object 的构造函数(在 Python 3 中可省略)

        self.model = keras.Sequential()
        # 创建一个 Keras 顺序模型(Sequential),用于堆叠神经网络层

        self.model.add(keras.layers.Dense(inNum, input_shape=(inNum,), activation='relu'))
        # 向模型中添加第一个全连接层(Dense)
        # 该层有 inNum 个神经元,输入形状为 (inNum,),即输入特征维度
        # 激活函数为 ReLU(修正线性单元),引入非线性

        self.model.add(keras.layers.Dense(outNum, activation='softmax'))
        # 向模型中添加第二个全连接层(输出层)
        # 该层有 outNum 个神经元,对应每个动作
        # 激活函数为 softmax,输出每个动作的概率分布(和为 1)

        self.model.compile(optimizer=tf.train.AdamOptimizer(0.001),
                           loss='categorical_crossentropy',
                           metrics=['accuracy'])
        # 编译模型,配置训练过程
        # 优化器:Adam,学习率为 0.001
        # 损失函数:分类交叉熵(categorical_crossentropy),适用于多分类问题
        # 评估指标:准确率(accuracy)

    def get_action(self, state):
        # 定义动作选择方法:根据当前状态选择动作
        # 参数 state:当前状态(形状应为 [1, inNum])

        return np.argmax(self.model.predict(state)[0])
        # 使用模型对状态进行预测,得到输出概率分布(shape: [1, outNum])
        # 取第一个样本(索引 0)的概率向量,然后用 argmax 找到最大概率对应的动作索引
        # 返回该动作索引(整数)

    def predict(self, next_state):
        # 定义预测方法:返回给定状态下所有动作的概率分布
        # 参数 next_state:下一状态(形状 [1, inNum])

        return self.model.predict(next_state)[0]
        # 对 next_state 进行预测,返回第一个样本的概率向量(长度 outNum)

    def fit(self, state, target, action):
        # 定义训练方法:使用一步 TD 目标更新网络
        # 参数 state:当前状态(形状 [1, inNum])
        # 参数 target:目标 Q 值(标量)
        # 参数 action:本次执行的动作索引(整数)

        target_f = self.model.predict(state)
        # 先对当前状态进行预测,得到当前网络输出的概率向量(shape: [1, outNum])

        target_f[0][action] = target
        # 将目标动作对应的输出概率替换为 target(注意:这里不是标准的 Q-learning 目标,而是直接修改概率值)

        self.model.fit(state, target_f, epochs=1, verbose=0)
        # 使用修改后的目标向量作为标签,对模型进行一次训练(epochs=1)
        # verbose=0 表示不输出训练过程信息

# Environment initialization
# 环境初始化部分

port = 5561
# 设置与 ns-3 仿真通信的端口号为 5561

simTime = 10 # seconds
# 设置仿真总时长为 10 秒

startSim = True
# 设置是否由 Python 启动 ns-3 仿真进程,True 表示自动启动

stepTime = 0.05 # seconds
# 设置每个 RL 步对应的仿真时间步长(0.05 秒)

seed = 132
# 设置随机种子,用于复现实验

simArgs = {"--simTime": simTime,
           "--testArg": 123,
           "--nodeNum": 5,
           "--distance": 500}
# 构建传递给 ns-3 可执行文件的命令行参数字典
# 包括仿真时长、测试参数、节点数、距离等

debug = False
# 是否开启调试模式,False 表示关闭

env = ns3env.Ns3Env(port=port, stepTime=stepTime, startSim=startSim, simSeed=seed, simArgs=simArgs, debug=debug)
# 创建 ns3-gym 环境实例,用于与 ns-3 仿真交互
# 参数分别指定端口、步长、是否自动启动、随机种子、仿真参数、调试模式

#env = gym.make('ns3-v0')
# 注释掉的代码:原本可能通过 gym 注册创建环境,但当前使用 ns3env 直接创建

ob_space = env.observation_space
# 获取环境的观察空间(observation space)

ac_space = env.action_space
# 获取环境的动作空间(action space)

print("Observation space: ", ob_space,  ob_space.dtype)
# 打印观察空间信息及其数据类型

print("Action space: ", ac_space, ac_space.dtype)
# 打印动作空间信息及其数据类型

s_size = ob_space.shape[0]
# 获取观察向量的维度(状态特征数量),例如 5

a_size = ac_space.shape[0]
# 获取动作向量的维度(动作数量),但这里并未直接使用,因为动作被离散化为 cwSize 类

inputQueues = 2
# 设置每个智能体的输入维度为 2(即观察状态中相邻两个队列长度)

cwSize = 100
# 设置每个智能体的动作空间大小(离散动作数),即 CW 值取 0~99 共 100 个

agent0 = DqnAgent(inputQueues, cwSize)
# 创建第一个智能体(控制 STA0),输入维度 2,输出维度 100

agent1 = DqnAgent(inputQueues, cwSize)
# 创建第二个智能体(控制 STA1)

agent2 = DqnAgent(inputQueues, cwSize)
# 创建第三个智能体(控制 STA2)

agent3 = DqnAgent(inputQueues, cwSize)
# 创建第四个智能体(控制 STA3)

total_episodes = 50
# 设置总训练回合数为 50

max_env_steps = 100
# 设置每个回合的最大步数为 100

env._max_episode_steps = max_env_steps
# 将环境的最大步数属性设置为 100,使回合在达到 100 步后自动结束

epsilon = 1.0               # exploration rate
# 初始化探索率 epsilon 为 1.0(完全随机探索)

epsilon_min = 0.01
# 设置探索率的最小值为 0.01

epsilon_decay = 0.999
# 设置探索率每步的衰减因子为 0.999

time_history = []
# 创建一个空列表,用于记录每个回合实际执行的步数(time)

rew_history = []
# 创建一个空列表,用于记录每个回合的累计奖励

for e in range(total_episodes):
    # 外层循环,遍历每个训练回合(episode)

    state = env.reset()
    # 重置环境,获得初始状态(一个长度为 s_size 的数组)

    state = np.reshape(state, [1, s_size])
    # 将状态重塑为二维形状 [1, s_size],便于作为神经网络输入

    rewardsum = 0
    # 初始化累计奖励为 0

    for time in range(max_env_steps):
        # 内层循环,每个回合最多执行 max_env_steps 步

        # Choose action
        # 选择动作

        if np.random.rand(1) < epsilon:
            # 如果随机数小于 epsilon,则进行随机探索

            action0 = np.random.randint(cwSize)
            # 智能体 0 随机选择一个动作(0~99)

            action1 = np.random.randint(cwSize)
            # 智能体 1 随机选择一个动作

            action2 = np.random.randint(cwSize)
            # 智能体 2 随机选择一个动作

            action3 = np.random.randint(cwSize)
            # 智能体 3 随机选择一个动作
        else:
            # 否则,利用当前策略选择动作(贪婪)

            action0 = agent0.get_action(state[:,0:2])
            # 智能体 0 根据状态的前两个元素(队列长度)选择动作

            action1 = agent1.get_action(state[:,1:3])
            # 智能体 1 根据状态的第 2、3 个元素选择动作

            action2 = agent2.get_action(state[:,2:4])
            # 智能体 2 根据状态的第 3、4 个元素选择动作

            action3 = agent3.get_action(state[:,3:5])
            # 智能体 3 根据状态的第 4、5 个元素选择动作

        # Step
        # 执行动作并推进仿真

        actionVec = [action0, action1, action2, action3, 100]
        # 构建动作向量:包含 4 个 STA 的 CW 值和 AP 的固定 CW 值 100

        next_state, reward, done, _ = env.step(actionVec)
        # 将动作向量发送给 ns-3,执行一个仿真步,返回下一状态、奖励、是否结束和其他信息

        if done:
            # 如果回合结束(达到最大步数或仿真终止)

            print("episode: {}/{}, time: {}, rew: {}, eps: {:.2}"
                  .format(e, total_episodes, time, rewardsum, epsilon))
            # 打印当前回合编号、总回合数、步数、累计奖励和探索率

            break
            # 跳出内层循环,结束该回合

        next_state = np.reshape(next_state, [1, s_size])
        # 将下一状态重塑为 [1, s_size] 形状

        # Train
        # 训练网络(更新 Q 值)

        target0 = reward
        # 初始化智能体 0 的目标值为当前奖励(若 done 则直接为奖励,否则后续加上折扣项)

        target1 = reward
        # 智能体 1 的目标值

        target2 = reward
        # 智能体 2 的目标值

        target3 = reward
        # 智能体 3 的目标值

        if not done:
            # 如果回合未结束,则计算 TD 目标:reward + γ * max Q(next_state)

            target0 = reward + 0.95 * np.amax(agent0.predict(next_state[:,0:2]))
            # 智能体 0:奖励 + 0.95 * 下一状态中前两个元素作为输入时网络输出的最大概率值
            # 注意:这里使用 np.amax 取最大概率,而非标准 Q 值(因输出是 softmax)

            target1 = reward + 0.95 * np.amax(agent1.predict(next_state[:,1:3]))
            # 智能体 1 类似

            target2 = reward + 0.95 * np.amax(agent2.predict(next_state[:,2:4]))
            # 智能体 2 类似

            target3 = reward + 0.95 * np.amax(agent3.predict(next_state[:,3:5]))
            # 智能体 3 类似

        agent0.fit(state[:,0:2], target0, action0)
        # 训练智能体 0:使用当前状态切片、目标值和执行的动作

        agent1.fit(state[:,1:3], target1, action1)
        # 训练智能体 1

        agent2.fit(state[:,2:4], target2, action2)
        # 训练智能体 2

        agent3.fit(state[:,3:5], target3, action3)
        # 训练智能体 3

        state = next_state
        # 更新当前状态为下一状态

        rewardsum += reward
        # 累计奖励

        if epsilon > epsilon_min: epsilon *= epsilon_decay
        # 如果探索率大于最小值,则按衰减因子减小 epsilon
        
    time_history.append(time)
    # 记录该回合实际执行的步数(注意 time 在 break 后保留跳出时的值)

    rew_history.append(rewardsum)
    # 记录该回合的累计奖励

#for n in range(2 ** s_size):
#    state = [n >> i & 1 for i in range(0, 2)]
#    state = np.reshape(state, [1, s_size])
#    print("state " + str(state) 
#        + " -> prediction " + str(model.predict(state)[0])
#        )
# 注释掉的代码:原本可能是用于测试所有可能状态的动作预测

#print(model.get_config())
#print(model.to_json())
#print(model.get_weights())
# 注释掉的代码:用于打印模型配置、JSON 表示或权重

plt.plot(range(len(time_history)), time_history)
# 绘制每个回合的步数曲线,x 轴为回合索引,y 轴为步数

plt.plot(range(len(rew_history)), rew_history)
# 在同一图上绘制每个回合的累计奖励曲线

plt.xlabel('Episode')
# 设置 x 轴标签为 Episode

plt.ylabel('Time')
# 设置 y 轴标签为 Time(实际上可能包含两条曲线,但这里标签设置为 Time,不太准确)

plt.show()
# 显示图形

curve0 = np.zeros(shape=(101,101))
# 创建一个 101x101 的零矩阵,用于存储智能体 0 在二维状态空间下的动作选择结果

curve1 = np.zeros(shape=(101,101))
# 智能体 1 的动作选择矩阵

curve2 = np.zeros(shape=(101,101))
# 智能体 2 的动作选择矩阵

curve3 = np.zeros(shape=(101,101))
# 智能体 3 的动作选择矩阵

for i in range(101):
    # 遍历第一个状态维度(0~100)

    for j in range(101):
        # 遍历第二个状态维度(0~100)

        state = np.array([i,j])
        # 构造一个包含两个元素的 NumPy 数组作为状态

        state = np.reshape(state, [1, 2])
        # 重塑为 [1,2] 形状,作为网络输入

        curve0[i,j] = agent0.get_action(state)
        # 获取智能体 0 在该状态下的动作索引,存入矩阵

        curve1[i,j] = agent1.get_action(state)
        # 智能体 1 的动作

        curve2[i,j] = agent2.get_action(state)
        # 智能体 2 的动作

        curve3[i,j] = agent3.get_action(state)
        # 智能体 3 的动作
        
print("Save curves to MATLAB file")
# 打印提示信息:保存曲线到 MATLAB 文件

io.savemat("curves_2d.mat", {
                '0':curve0,
                '1':curve1,
                '2':curve2,
                '3':curve3,
               }
          )
# 使用 scipy.io.savemat 将四个矩阵保存为 MATLAB 格式文件 curves_2d.mat
# 字典的键为 '0','1','2','3',对应四个智能体的动作选择曲面

import一些库

都是以前说过的了,唯一需要注意的两个东西,一是keras,做全链接用的库,没有他,DQN的D就无从谈起,另一个是ns3env,用来链接"实测环境"的"socket"

python 复制代码
import scipy.io as io
# 导入 scipy.io 模块并命名为 io,用于保存和加载 MATLAB 格式文件(.mat)

import gym
# 导入 OpenAI Gym 库,提供强化学习环境的标准接口(尽管本代码未直接使用 gym 创建环境)

import tensorflow as tf
# 导入 TensorFlow 深度学习框架

import tensorflow.contrib.slim as slim
# 导入 TensorFlow 的 contrib.slim 模块(用于简化网络定义,但本代码未实际使用)

import numpy as np
# 导入 NumPy 库,用于数值计算和数组操作

import matplotlib.pyplot as plt
# 导入 Matplotlib 的 pyplot 模块,用于绘图

from tensorflow import keras
# 从 TensorFlow 中导入 Keras API,用于构建和训练神经网络模型

from ns3gym import ns3env
# 从 ns3gym 包中导入 ns3env 模块,用于创建与 ns-3 仿真环境交互的 Gym 环境

DQN基础知识

首先DQN的前置知识是表格式Q-learning

Q-learning标准流程

我们知道Qlearning是按照如下思路更新的

Q(s,a) \leftarrow Q(s,a) + \alpha \Bigl r + \\gamma \\max_{a'} Q(s', a') - Q(s,a) \\Bigr

  • 状态和动作

s是状态,a是动作,状态s是agent看到的信息,a是根据状态s,agent选的动作

s'是下一个step的状态,a'是遍历s'下的所有动作,用来使得Q(s,a)也考虑后面的状态,保证长期奖励

  • Q表

Q(s,a) 就是我们要反复更新、积累经验的多维数组,里面存储着每一个agent针对每一个状态s作出的每一个动作a的长期回报估计值,数值越大说明当前agent遇到s使用a越有利

Q(s', a')代表了下一个step评价,越大说明,下一个step到达了s'选取动作a'越合适,max的意思是本step要看下一step最好能是啥样的Q

\max_{a'} Q(s', a')越大,越说明当前状态s如果选了a(那么下一步到了s'选取动作a'取得的Q越大),也就是说下一步获得的长期奖励更好(Q-learning 正是通过这种只向后看一步的 TD 更新,将长期回报信息逐步向前传导,从而逼近全局最优策略。)

  • 学习率

\alpha就是学习率,0~1,越大Q的变化越剧烈,导致振荡更大,最开始的学习可能很快也可能很快的选了不合适的action获得小reward

  • 奖励

r就是奖励,每个step都会有一个奖励,本轮次step选择动作a的奖励

  • 折扣率

\gamma是折扣率,0~1,越大越重视下一轮的Q,越强调下一轮Q重要,越小,比如取0,此时Q= \alpha*r,也就是只考虑本step的奖励

  • 创建Q表,确定state范围,比如1-10,一维,确定action范围,比如1-5,一维,确定agent数目,比如5个,那么Q表就是一个3维表,可以理解为5张表,每个表10*5大小,比如123,就是1号agent,在状态2下,选择动作3的Q值
  • 选择学习率、折扣率,使得符合对训练效率&波动幅度、近期优势&长期优势的权衡
  • 映射动作、状态、奖励,使他们与现实情况一一对应
  • 开始循环,分为多个episode,每个episode对应的都应该是完全一样的环境(不要求环境每次变化都稳定,有随机变化的环境也可以,Q稳定之后就说明对于训练这种随机变换环境当前Q以经取得了最好结果了),每个episode里面对应多step
  • 每个step,对于ε-贪婪策略,在探索阶段,动作更多选择为随机值(就比如1-5的随机值),在稳定阶段,就选择Q表推荐的最优action,选择了action就会导致一个新的reward的获得、新的状态state的获得、是否gameover
  • 新的reward和新的state可以用来算r + \gamma \max_{a'} Q(s', a') - Q(s,a),新的reward就是本step选action的结果,新的state就是s',我们遍历a'就可以获得下一个状态的最好的期待(也就是off policy),也就完成了本step的Q更新
  • 反复迭代step,完成一个episode,(也就是说一个episode只是一条线,并没有看到每一个step的所有的s的可能性,所以才需要多episode取得最优,也就是说明了Qlearning属于TD时序差分方法),迭代多个episode,稳定获得最优值
  • 衡量效果好不好的方法是看每个episode的总reward是不是较快的收敛到了最大值,收敛的越快越好、收敛后的总reward越高越好。
DQN的标准流程

对于**Q-learning,**当状态空间巨大或连续时,Q 表的存储和更新将变得不可行,这就是所谓的"维度灾难"。

深度 Q 网络(DQN)通过使用深度神经网络作为函数逼近器,从根本上解决了这一问题。DQN 将 Q(s,a)参数化为 Qθ(s,a),其中 θ 表示网络权重。网络输入状态 s,输出所有动作的 Q 值(或每个动作一个输出节点)。训练目标是使网络输出尽可能接近 TD 目标,通常采用均方误差损失:

这里 θ表示目标网络的参数,它定期从在线网络 θθ 复制而来,用于稳定训练。梯度下降更新参数:

与表格方法直接修改单个 Q 值不同,DQN 的一次参数更新会影响所有状态-动作对的估计,这赋予了 DQN 强大的泛化能力:相似的状态会得到相似的 Q 值输出,无需显式枚举所有状态。

此外,DQN 引入了经验回放(Experience Replay)机制。智能体将每一步的转移样本 (s,a,r,s′)存入一个回放缓冲区 D,训练时从缓冲区中随机采样小批量数据进行更新。这样做打破了连续样本之间的相关性,提高了数据利用率,并使训练过程更加稳定。

事实上, DQN 代码(如 dqn-agent-v1.py)虽然使用了神经网络,但缺失了经验回放和目标网络,并且输出层采用 softmax 激活函数和交叉熵损失,与标准 DQN 的线性输出和 MSE 回归有所不同。其更新逻辑更接近在线 Q-learning 的函数近似版本:每交互一步即用当前网络计算 TD 目标(但将 softmax 输出概率当作 Q 值),然后通过梯度下降让网络输出向该目标靠拢。这种实现仍属于深度强化学习的范畴,但稳定性和收敛性可能不如完整 DQN。

总结来说,DQN 相较于表格型 Q-learning 的本质变化在于:用参数化神经网络替代显式 Q 表,从而能够处理高维连续状态空间,并借助经验回放和目标网络提升学习稳定性和样本效率。这一转变使得强化学习从只能解决小规模离散问题,迈向了能够应对复杂真实环境(如图像、网络控制等)的通用框架。

两个DQN和Qlearning最大的区别

两个最大的区别分别是,

没有显式的Q表,而是用全链接层模拟Q函数------事实上Q表和Q函数的目的都是,输入action和state,输出Q值,只要能满足这两个条件,事实上都差不多

可以看到model选择了顺序模型,各个层会一层一层连起来,事实上只有一个输入和一个输出层,都是全链接,第一层输入2维的向量(相邻node的缓冲区)然后RELU做一下非线性,第二层输出一个100维的向量(选择0-99这些CW值的概率------因为做了softmax------如果不加softmax应该是各个CW对应的Q值),然后规定了一下训练过程中反向传播时的优化器、损失函数和评估指标

接下来包装了action获取函数,把返回的1,100的张量提取出100维的向量,然后看哪个action对应的概率最大,就取哪个action进行返回,此时返回的就是一个标量了,一个0-100的值,对应的是CW的值

接下来包装了新的state对应的概率预测函数,预测了好作为未来的Q函数目标、好去逼近------也就是训练,具体的操作其实只是当前的Q函数对于当前的state输出的100个动作对应的概率(归一化Q值)的一个100维向量

接下来是fit函数的包装,需要的action、当state和目标,目标是一个标量,所以先取当前state下所有action对的归一化Q,接下来把想更新的target替换进去,最后升维并交给真正的fit

复制代码
class DqnAgent(object):
    """docstring for DqnAgent"""
    # 定义一个 DQN 智能体类,继承自 object(Python 3 中所有类默认继承 object)
    # 类的文档字符串(docstring)

    def __init__(self, inNum, outNum):
        # 类的构造函数,用于初始化 DQN 智能体
        # 参数 inNum:输入维度(状态特征数)
        # 参数 outNum:输出维度(动作数)

        super(DqnAgent, self).__init__()
        # 调用父类 object 的构造函数(在 Python 3 中可省略)

        self.model = keras.Sequential()
        # 创建一个 Keras 顺序模型(Sequential),用于堆叠神经网络层

        self.model.add(keras.layers.Dense(inNum, input_shape=(inNum,), activation='relu'))
        # 向模型中添加第一个全连接层(Dense)
        # 该层有 inNum 个神经元,输入形状为 (inNum,),即输入特征维度
        # 激活函数为 ReLU(修正线性单元),引入非线性

        self.model.add(keras.layers.Dense(outNum, activation='softmax'))
        # 向模型中添加第二个全连接层(输出层)
        # 该层有 outNum 个神经元,对应每个动作
        # 激活函数为 softmax,输出每个动作的概率分布(和为 1)

        self.model.compile(optimizer=tf.train.AdamOptimizer(0.001),
                           loss='categorical_crossentropy',
                           metrics=['accuracy'])
        # 编译模型,配置训练过程
        # 优化器:Adam,学习率为 0.001
        # 损失函数:分类交叉熵(categorical_crossentropy),适用于多分类问题
        # 评估指标:准确率(accuracy)

    def get_action(self, state):
        # 定义动作选择方法:根据当前状态选择动作
        # 参数 state:当前状态(形状应为 [1, inNum])

        return np.argmax(self.model.predict(state)[0])
        # 使用模型对状态进行预测,得到输出概率分布(shape: [1, outNum])
        # 取第一个样本(索引 0)的概率向量,然后用 argmax 找到最大概率对应的动作索引
        # 返回该动作索引(整数)

    def predict(self, next_state):
        # 定义预测方法:返回给定状态下所有动作的概率分布
        # 参数 next_state:下一状态(形状 [1, inNum])

        return self.model.predict(next_state)[0]
        # 对 next_state 进行预测,返回第一个样本的概率向量(长度 outNum)

    def fit(self, state, target, action):
        # 定义训练方法:使用一步 TD 目标更新网络
        # 参数 state:当前状态(形状 [1, inNum])
        # 参数 target:目标 Q 值(标量)
        # 参数 action:本次执行的动作索引(整数)

        target_f = self.model.predict(state)
        # 先对当前状态进行预测,得到当前网络输出的概率向量(shape: [1, outNum])

        target_f[0][action] = target
        # 将目标动作对应的输出概率替换为 target(注意:这里不是标准的 Q-learning 目标,而是直接修改概率值)

        self.model.fit(state, target_f, epochs=1, verbose=0)
        # 使用修改后的目标向量作为标签,对模型进行一次训练(epochs=1)
        # verbose=0 表示不输出训练过程信息
Q函数的更新

与上面的操作相辅相成,target的获得与表格式Qlearning一致,但表格式的target算出来后直接更新到Q表里面了,这里则需要再走一下fit,让全链接拟合的Q函数向Target靠拢。

复制代码
 target0 = reward
        # 初始化智能体 0 的目标值为当前奖励(若 done 则直接为奖励,否则后续加上折扣项)

        target1 = reward
        # 智能体 1 的目标值

        target2 = reward
        # 智能体 2 的目标值

        target3 = reward
        # 智能体 3 的目标值

        if not done:
            # 如果回合未结束,则计算 TD 目标:reward + γ * max Q(next_state)

            target0 = reward + 0.95 * np.amax(agent0.predict(next_state[:,0:2]))
            # 智能体 0:奖励 + 0.95 * 下一状态中前两个元素作为输入时网络输出的最大概率值
            # 注意:这里使用 np.amax 取最大概率,而非标准 Q 值(因输出是 softmax)

            target1 = reward + 0.95 * np.amax(agent1.predict(next_state[:,1:3]))
            # 智能体 1 类似

            target2 = reward + 0.95 * np.amax(agent2.predict(next_state[:,2:4]))
            # 智能体 2 类似

            target3 = reward + 0.95 * np.amax(agent3.predict(next_state[:,3:5]))
            # 智能体 3 类似

        agent0.fit(state[:,0:2], target0, action0)
        # 训练智能体 0:使用当前状态切片、目标值和执行的动作

        agent1.fit(state[:,1:3], target1, action1)
        # 训练智能体 1

        agent2.fit(state[:,2:4], target2, action2)
        # 训练智能体 2

        agent3.fit(state[:,3:5], target3, action3)
        # 训练智能体 3

对应python的一次训练模拟

除了这两个大点(两个DQN和Qlearning最大的区别),其他的流程都与表格Qlearning类似,为了更好的理解,我们来模拟一次训练:

假设环境已初始化,网络参数随机。我们跟踪第一个 episode 的前 3 个 step。仿真参数:nWifi=4(4 个 STA),总节点 5(AP + 4 STA),cwSize=100epsilon=1.0(初始)。


Step 0(episode 开始)

1. 重置环境,获取初始状态
复制代码
state = env.reset()
state = np.reshape(state, [1, s_size])

示例值:ns-3 返回的初始队列长度为:

复制代码
[20, 35, 12, 40, 8]   # 对应节点0(AP),1(STA0),2(STA1),3(STA2),4(STA3)

reshape 后 state = [[20, 35, 12, 40, 8]],形状 (1,5)

2. 动作选择(ε-贪婪)
复制代码
if np.random.rand(1) < epsilon:
    action0 = np.random.randint(cwSize)
    action1 = np.random.randint(cwSize)
    action2 = np.random.randint(cwSize)
    action3 = np.random.randint(cwSize)
else:
    action0 = agent0.get_action(state[:,0:2])
    ...

当前 epsilon=1.0,随机数假设为 0.123(<1.0),所以执行随机分支:

  • action0 = 23(随机值)

  • action1 = 67

  • action2 = 5

  • action3 = 91

3. 构造动作向量并执行环境步
复制代码
actionVec = [action0, action1, action2, action3, 100]
next_state, reward, done, _ = env.step(actionVec)

动作向量:[23, 67, 5, 91, 100]

ns-3 将这些值依次赋给节点 0,1,2,3,4 的 CW。仿真推进 0.05s。

返回:

  • next_state = [18, 30, 15, 38, 10]

  • reward = 3.0(这段时间 AP 成功收到 3 个包)

  • done = False

4. 重塑 next_state
复制代码
next_state = np.reshape(next_state, [1, s_size])

得到 next_state = [[18, 30, 15, 38, 10]]

5. 计算 TD 目标并训练每个 agent
复制代码
# 计算 target0(agent0)
target0 = reward
if not done:
    target0 = reward + 0.95 * np.amax(agent0.predict(next_state[:,0:2]))

agent0 部分

  • 输入切片:next_state[:,0:2] = [[18, 30]]

  • agent0.predict([[18,30]]) 返回一个 100 维概率向量(假设网络初始输出接近均匀,最大值为 0.012)

  • np.amax = 0.012

  • target0 = 3.0 + 0.95 * 0.012 = 3.0114

其他 agent 类似,但 target 值相同(因为 reward 相同且预测最大概率相近):

  • target1 = 3.0114

  • target2 = 3.0114

  • target3 = 3.0114

然后调用 fit

复制代码
agent0.fit(state[:,0:2], target0, action0)
  • state[:,0:2] = [[20, 35]]

  • fit 内部:

    1. target_f = self.model.predict(state[:,0:2]) → 得到 100 维概率向量。

    2. target_f[0][action0] = target0 → 将索引 23 处的值替换为 3.0114。

    3. self.model.fit(state[:,0:2], target_f, epochs=1, verbose=0) → 用修改后的向量作为目标,对网络进行一次梯度更新。

同理:

复制代码
agent1.fit(state[:,1:3], target1, action1)
agent2.fit(state[:,2:4], target2, action2)
agent3.fit(state[:,3:5], target3, action3)
6. 更新状态和 epsilon
复制代码
state = next_state
rewardsum += reward
if epsilon > epsilon_min: epsilon *= epsilon_decay
  • state 变为 [[18, 30, 15, 38, 10]]

  • rewardsum = 3.0

  • epsilon = 1.0 * 0.999 = 0.999


Step 1(同一 episode 内)

1. 动作选择

当前 epsilon = 0.999,假设随机数 0.65(>0.999? 不对,0.65 < 0.999,所以仍然随机)。实际上,在 step1 时,如果随机数小于 epsilon,则继续随机;否则利用。为展示利用分支,我们假设随机数 0.99 > 0.999(实际 0.99 < 0.999 是小于,我们调整假设:0.9995 > 0.999,则走利用分支)。所以我们假设随机数 0.9995 > epsilon,执行 else 分支:

复制代码
else:
    action0 = agent0.get_action(state[:,0:2])
    action1 = agent1.get_action(state[:,1:3])
    action2 = agent2.get_action(state[:,2:4])
    action3 = agent3.get_action(state[:,3:5])

状态切片 (使用当前 state = [[18, 30, 15, 38, 10]]):

  • agent0.get_action([[18,30]]):网络输出概率向量,假设最大概率索引为 30 → action0=30

  • agent1.get_action([[30,15]]) → 假设 action1=12

  • agent2.get_action([[15,38]]) → 假设 action2=88

  • agent3.get_action([[38,10]]) → 假设 action3=45

2. 执行动作
复制代码
actionVec = [30, 12, 88, 45, 100]
next_state, reward, done, _ = env.step(actionVec)

假设返回:

  • next_state = [22, 28, 11, 35, 9]

  • reward = 4.0

  • done = False

3. 重塑 next_state
复制代码
next_state = np.reshape(next_state, [1, s_size])
4. 计算 TD 目标并训练

以 agent0 为例:

  • 输入切片 next_state[:,0:2] = [[22,28]]

  • agent0.predict([[22,28]]) 输出概率向量,假设最大值为 0.015

  • target0 = 4.0 + 0.95 * 0.015 = 4.01425

  • fit 类似。

其他 agent 同样更新。

5. 更新状态和 epsilon
  • state 变为 [[22, 28, 11, 35, 9]]

  • rewardsum += 4.0 → 累计 7.0

  • epsilon *= 0.999 → 0.998001


Step 2(继续)

1. 动作选择

假设现在随机数 0.5 < epsilon(0.998),所以走随机分支:

复制代码
action0 = np.random.randint(cwSize)  # 假设 42
action1 = np.random.randint(cwSize)  # 假设 7
action2 = np.random.randint(cwSize)  # 假设 63
action3 = np.random.randint(cwSize)  # 假设 81
2. 执行动作
复制代码
actionVec = [42, 7, 63, 81, 100]
next_state, reward, done, _ = env.step(actionVec)

返回:

  • next_state = [25, 33, 14, 40, 12]

  • reward = 2.0

  • done = False

3. 训练
  • 各 agent 计算 target 并 fit。

  • 例如 agent0: target0 = 2.0 + 0.95 * max(agent0.predict([[25,33]]))

4. 更新状态和 epsilon
  • state = [[25, 33, 14, 40, 12]]

  • rewardsum += 2.0 → 9.0

  • epsilon 继续衰减。


关键点总结

  • 状态切片state[:,0:2] 取相邻两个节点的队列长度作为 agent 的输入。

  • 动作:每个 agent 输出一个离散 CW 索引(0~99),动作向量组装后发送给 ns-3。

  • TD 目标reward + 0.95 * np.amax(agent.predict(next_state_slice)),其中 np.amax 取的是网络输出概率向量的最大值(近似 Q 值)。

  • 训练 :通过 fit 修改网络输出目标,进行一次梯度更新,实现 Q 函数的迭代逼近。

这个流程展示了 DQN 在线学习的基本步骤,尽管缺少经验回放和目标网络,但其核心更新逻辑与表格 Q-learning 一致。

Episode 0 最后一步到 Episode 1 第一步的详细流程,承接上一步骤

  • total_episodes = 50max_env_steps = 100

  • Episode 0 没有提前触发 done,因此执行了完整的 100 步(step 0 到 step 99)。

  • 在 Episode 0 的最后一步(Step 99)时,epsilon 已经过 100 次衰减:epsilon = 1.0 * 0.999^100 ≈ 0.9048

  • 每个 agent 的神经网络已根据之前的交互进行了多次更新,但具体参数未知,我们只关注流程。


Episode 0, Step 99(最后一个 step)

1. 当前状态(来自 Step 98 的 next_state

假设经过前面的步骤,当前 state 为:

复制代码
state = [[15, 22, 30, 18, 25]]   # 节点0~4的队列长度

(实际值可能是任意整数,此处取合理示例)

2. 动作选择(ε-贪婪)
复制代码
if np.random.rand(1) < epsilon:   # epsilon ≈ 0.9048
    # 随机分支的概率很高
    action0 = np.random.randint(cwSize)  # 假设 55
    action1 = np.random.randint(cwSize)  # 假设 80
    action2 = np.random.randint(cwSize)  # 假设 10
    action3 = np.random.randint(cwSize)  # 假设 37
else:
    # 利用分支(本例可能不会执行)
    ...

由于 epsilon 仍然较大,随机分支被触发的概率高。我们假设随机数小于 0.9048,因此选择了随机动作。

3. 构造动作向量并执行
复制代码
actionVec = [55, 80, 10, 37, 100]   # 最后一位固定给 AP
next_state, reward, done, _ = env.step(actionVec)

ns-3 执行仿真步(0.05 秒),返回:

  • next_state = [12, 19, 26, 15, 22]

  • reward = 5.0

  • done = False(因为仿真未结束且 MyGetGameOver 始终返回 false)

4. 重塑 next_state
复制代码
next_state = np.reshape(next_state, [1, s_size])
# 得到 [[12, 19, 26, 15, 22]]
5. 计算 TD 目标并训练各 agent

以 agent0 为例:

  • 输入切片:next_state[:,0:2] = [[12, 19]]

  • agent0.predict([[12,19]]) 输出一个 100 维概率向量(经过之前训练,可能不再是均匀分布)。假设最大概率值为 0.035

  • target0 = reward + 0.95 * np.amax(...) = 5.0 + 0.95 * 0.035 = 5.03325

其他 agent 类似,假设它们的预测最大值也接近,得到相近的 target。

然后调用:

复制代码
agent0.fit(state[:,0:2], target0, action0)   # state[:,0:2] = [[15,22]]
agent1.fit(state[:,1:3], target1, action1)   # [[22,30]]
agent2.fit(state[:,2:4], target2, action2)   # [[30,18]]
agent3.fit(state[:,3:5], target3, action3)   # [[18,25]]

每个 fit 内部:先用当前状态切片预测概率向量,将 action 对应位置的值替换为 target,然后执行一次梯度更新。

6. 更新状态、累计奖励、衰减 epsilon
复制代码
state = next_state          # state 变为 [[12, 19, 26, 15, 22]]
rewardsum += reward         # rewardsum 累加 5.0(此处假设 episode 累计总奖励为 200.0 左右)
if epsilon > epsilon_min:
    epsilon *= epsilon_decay  # epsilon = 0.9048 * 0.999 ≈ 0.9039
7. 内层循环结束

因为 time 已经达到 99(最后一个索引),循环自然结束(没有 break)。此时:

复制代码
time_history.append(time)      # 记录 time = 99
rew_history.append(rewardsum)  # 记录本 episode 的累计奖励

Episode 0 结束。


Episode 1 开始(Step 0)

1. 外层循环进入下一个 episode
复制代码
for e in range(1, total_episodes):   # e = 1
    state = env.reset()
    state = np.reshape(state, [1, s_size])
    rewardsum = 0
2. env.reset() 的作用
  • 重置 ns-3 环境至初始状态(仿真时间归零、队列清空、随机种子可能重置?注意 ns-3 可能没有完全重置,但此处简化)。

  • 返回一个新的观测向量。假设初始队列长度与 Episode 0 不同,例如:

复制代码
state = [10, 15, 20, 25, 30]   # 返回的是 1D 数组

重塑后:state = [[10, 15, 20, 25, 30]]

3. 动作选择(ε-贪婪)

此时 epsilon 继承自 Episode 0 结束时的值(约 0.9039),并未因新 episode 而重置。

复制代码
if np.random.rand(1) < epsilon:   # 仍然很大,随机概率高
    action0 = np.random.randint(cwSize)   # 假设 3
    action1 = np.random.randint(cwSize)   # 假设 61
    action2 = np.random.randint(cwSize)   # 假设 44
    action3 = np.random.randint(cwSize)   # 假设 95
else:
    # 利用分支,本例可能不执行
    ...

假设随机数小于 epsilon,选择随机动作。

4. 构造动作向量并执行
复制代码
actionVec = [3, 61, 44, 95, 100]
next_state, reward, done, _ = env.step(actionVec)

返回:

  • next_state = [8, 12, 18, 22, 27]

  • reward = 2.0

  • done = False

5. 训练

与之前步骤类似,计算各 agent 的 target 并 fit

6. 更新状态、累计奖励、衰减 epsilon
复制代码
state = next_state
rewardsum += reward
epsilon *= epsilon_decay   # epsilon 继续下降

关键观察

  • 状态连续性 :Episode 0 结束时的 state[[12,19,26,15,22]],但 Episode 1 开始时通过 env.reset() 获得了一个全新的初始状态 [[10,15,20,25,30]],而不是接着 Episode 0 的最终状态。这是因为环境被重置。

  • epsilon 持续性epsilon 不会在 episode 之间重置,它始终在 Python 循环中持续衰减。因此 Episode 1 的初始探索率等于 Episode 0 结束时的值(约 0.9039),继续衰减直到 epsilon_min

  • 网络参数持续性 :各 agent 的神经网络权重在 episode 之间保持不变,并继续通过训练更新。因此 Episode 1 中 get_action 的行为已受到之前训练的影响。

这个流程完整展示了从 Episode 0 的最后一步到 Episode 1 的第一步的代码执行顺序和数值变化。

引用(一个是公开一个是acm,其实是同一篇):

Gawłowicz P, Zubow A. Ns-3 meets openai gym: The playground for machine learning in networking researchC//Proceedings of the 22nd International ACM Conference on Modeling, Analysis and Simulation of Wireless and Mobile Systems. 2019: 113-120.

Zubow A. ns3-gym: Extending openai gym for networking researchJ. arXiv preprint arXiv:1810.03943, 2018.

相关推荐
weixin_440730501 小时前
playwright实战-渠道应用操作
开发语言·前端·python
asdzx671 小时前
Python 解析 Excel 数据、图片与图表的实现方案
python·excel
砚底藏山河1 小时前
存储选型实战:CSV-SQLite-MySQL同机基准(魔码量化实战 #02)
java·数据库·python·金融·maven
陈年老古董1 小时前
Python模拟MapReduce分治思想 | 从单文件统计到大文件拆分聚合 学习笔记
开发语言·hadoop·笔记·python·学习
quantdash_cc1 小时前
数据 API 的稳定性应该如何长期监控?从量化数据监控体系到 QuantDash 实践
开发语言·python·数据分析·量化交易·股票数据·quantdash
水龙吟啸1 小时前
华为研发岗AI方向9.9机考题复盘&分析
人工智能·python·算法·华为
nanawinona1 小时前
先跑通小流程,再扩展量化功能
人工智能·python
hongyucai1 小时前
一个碗引发的血案
python·几何学·拓扑学
佳児素花痴╮1 小时前
C++速通2
开发语言·c++·算法