30分钟吃掉DQN算法

news2024/11/25 3:21:06

9ef2a3782ebcdf75fca0c11c24483fd5.png

表格型方法存储的状态数量有限,当面对围棋或机器人控制这类有数不清的状态的环境时,表格型方法在存储和查找效率上都受局限,DQN的提出解决了这一局限,使用神经网络来近似替代Q表格

本质上DQN还是一个Q-learning算法,更新方式一致。为了更好的探索环境,同样的也采用epsilon-greedy方法训练。

在Q-learning的基础上,DQN提出了两个技巧使得Q网络的更新迭代更稳定。

  • 经验回放(Experience Replay): 使用一个经验池存储多条经验s,a,r,s',再从中随机抽取一批数据送去训练。

  • 固定目标(Fixed Q-Target): 复制一个和原来Q网络结构一样的Target-Q网络,用于计算Q目标值。

公众号算法美食屋后台回复关键词:torchkeras,获取本文notebook源码~

不了解强化学习的同学,推荐先阅读:Q-learning解决悬崖问题

一,准备环境

gym是一个常用的强化学习测试环境,可以用make创建环境。

env具有reset,step,render几个方法。

倒立摆问题

环境设计如下:

倒立摆问题环境的状态是无限的,用一个4维的向量表示state.

4个维度分别代表如下含义

  • cart位置:-2.4 ~ 2.4

  • cart速度:-inf ~ inf

  • pole角度:-0.5 ~ 0.5 (radian)

  • pole角速度:-inf ~ inf

智能体设计如下:

智能体的action有两种,可能的取值2种:

  • 0,向左

  • 1,向右

奖励设计如下:

每维持一个步骤,奖励+1,到达200个步骤,游戏结束。

所以最高得分为200分。

倒立摆问题希望训练一个智能体能够尽可能地维持倒立摆的平衡。

import gym 
import numpy as np 
import pandas as pd 
import time
import matplotlib
import matplotlib.pyplot as plt
from IPython import display

print("gym.__version__=",gym.__version__)


%matplotlib inline

#可视化函数:
def show_state(env, step, info=''):
    plt.figure(num=10086,dpi=100)
    plt.clf()
    plt.imshow(env.render())
    plt.title("step: %d %s" % (step, info))
    plt.axis('off')
    display.clear_output(wait=True)
    display.display(plt.gcf())
    plt.close()
    

env = gym.make('CartPole-v1',render_mode="rgb_array") # CartPole-v0: 预期最后一次评估总分 > 180(最大值是200)
env.reset()
action_dim = env.action_space.n   # CartPole-v0: 2
obs_shape = env.observation_space.shape   # CartPole-v0: (4,)
gym.__version__= 0.26.2
env.reset()
done = False
step = 0
while not done:
    
    action = np.random.randint(0, 1)
    state,reward,done,truncated,info = env.step(action)
    step+=1
    print(state,reward)
    time.sleep(1.0)
    #env.render() 
    show_state(env,step=step)
    #print('step {}: action {}, state {}, reward {}, done {}, truncated {}, info {}'.format(\
    #        step, action, state, reward, done, truncated,info))
    
display.clear_output(wait=True)

0600aa2e49580d2568fae93d51fc8b93.gif

可以看到,没有训练智能体之前,我们采取随机动作的话,只维持了10步,倒立摆就因为倾斜角度超出范围而导致游戏结束。😭

二,定义Agent

DQN的核心思想为使用一个神经网络来近似替代Q表格。

Model: 模型结构, 负责拟合函数 Q(s,a)。主要实现forward方法。

Agent:智能体,负责学习并和环境交互, 输入输出是numpy.array形式。有sample(单步采样), predict(单步预测), 有predict_batch(批量预测), compute_loss(计算损失), sync_target(参数同步)等方法。

import torch 
from torch import nn
import torch.nn.functional as F
import copy 

class Model(nn.Module):
    def __init__(self, obs_dim, action_dim):
        
        # 3层全连接网络
        super(Model, self).__init__()
        self.obs_dim = obs_dim
        self.action_dim = action_dim 
        self.fc1 = nn.Linear(obs_dim,32)
        self.fc2 = nn.Linear(32,16)
        self.fc3 = nn.Linear(16,action_dim)

    def forward(self, obs):
        # 输入state,输出所有action对应的Q,[Q(s,a1), Q(s,a2), Q(s,a3)...]
        x = self.fc1(obs)
        x = torch.tanh(x)
        x = self.fc2(x)
        x = torch.tanh(x)
        Q = self.fc3(x)
        return Q
    
model = Model(4,2)
model_target = copy.deepcopy(model)

model.eval()
model.forward(torch.tensor([[0.2,0.1,0.2,0.0],[0.3,0.5,0.2,0.6]]))

model_target.eval() 
model_target.forward(torch.tensor([[0.2,0.1,0.2,0.0],[0.3,0.5,0.2,0.6]]))
tensor([[-0.1148,  0.0068],
        [-0.1311,  0.0315]], grad_fn=<AddmmBackward0>)
import torch 
from torch import nn 
import copy 

class DQNAgent(nn.Module):
    def __init__(self, model, 
        gamma=0.9,
        e_greed=0.1,
        e_greed_decrement=0.001
        ):
        super().__init__()
        
        self.model = model
        self.target_model = copy.deepcopy(model)
  
        self.gamma = gamma # reward 的衰减因子,一般取 0.9 到 0.999 不等
        
        self.e_greed = e_greed  # 有一定概率随机选取动作,探索
        self.e_greed_decrement = e_greed_decrement  # 随着训练逐步收敛,探索的程度慢慢降低
        
        self.global_step = 0
        self.update_target_steps = 200 # 每隔200个training steps再把model的参数复制到target_model中
        
        
    def forward(self,obs):
        return self.model(obs)
    
    @torch.no_grad()
    def predict_batch(self, obs):
        """ 使用self.model网络来获取 [Q(s,a1),Q(s,a2),...]
        """
        self.model.eval()
        return self.forward(obs)
    
    
    #单步骤采样    
    def sample(self, obs):
        sample = np.random.rand()  # 产生0~1之间的小数
        if sample < self.e_greed:
            action = np.random.randint(self.model.action_dim)  # 探索:每个动作都有概率被选择
        else:
            action = self.predict(obs)  # 选择最优动作
        self.e_greed = max(
            0.01, self.e_greed - self.e_greed_decrement)  # 随着训练逐步收敛,探索的程度慢慢降低
        return action
    
    #单步骤预测   
    def predict(self, obs):  # 选择最优动作
        obs = np.expand_dims(obs, axis=0)
        tensor = torch.tensor(obs,dtype=torch.float32).to(self.model.fc1.weight.device)
        pred_Q = self.predict_batch(tensor)
        action = torch.argmax(pred_Q,1,keepdim=True).cpu().numpy()  
        action = np.squeeze(action)
        return action
    
    
    def sync_target(self):
        """ 把 self.model 的模型参数值同步到 self.target_model
        """
        self.target_model.load_state_dict(self.model.state_dict())
    

    def compute_loss(self, obs, action, reward, next_obs, done):
        
        # 每隔200个training steps同步一次model和target_model的参数
        if self.global_step % self.update_target_steps == 0:
            self.sync_target()
        self.global_step += 1
        
        
        # 从target_model中获取 max Q' 的值,用于计算target_Q
        self.target_model.eval()
        next_pred_value = self.target_model(next_obs)
        best_value = torch.max(next_pred_value, dim = 1,keepdim=True).values 
        target = reward.reshape((-1,1)) + (
            torch.tensor(1.0) - done.reshape(-1,1)) * self.gamma * best_value
        
        #print("best_value",best_value.shape)
        #print("target",target.shape)

        # 获取Q预测值
        self.model.train()
        pred_value = self.model(obs)  
        action_onehot = F.one_hot(action.reshape(-1),
                num_classes = self.model.action_dim).float()
        prediction = torch.sum(pred_value*action_onehot,dim= 1,keepdim=True)
        
        #print("pred_value",pred_value.shape)
        #print("action_onehot",action_onehot.shape)
        #print("prediction",prediction.shape)
        
        # 计算 Q(s,a) 与 target_Q的均方差,得到loss
        loss = F.smooth_l1_loss(target,prediction)
        return loss
agent = DQNAgent(model,gamma=0.9,e_greed=0.1,
                 e_greed_decrement=0.001)
agent.predict_batch(torch.tensor([[2.0,3.0,4.0,2.0],[1.0,2.0,3.0,4.0]]))
tensor([[-0.1596, -0.0481],
        [-0.0927,  0.0318]])
loss = agent.compute_loss(torch.tensor([[2.0,3.0,4.0,2.0],[1.0,2.0,3.0,4.0],[1.0,2.0,3.0,4.0]]),
          torch.tensor([[1],[0],[0]]),
          torch.tensor([[1.0],[1.0],[1.0]]),
         torch.tensor([[2.0,3.0,0.4,2.0],[1.0,2.0,3.0,4.0],[1.0,2.0,3.0,4.0]]),
         torch.tensor(0.9))
print(loss)
tensor(0.5757, grad_fn=<SmoothL1LossBackward0>)

三,训练Agent

import random
import collections
import numpy as np

LEARN_FREQ = 5 # 训练频率,不需要每一个step都learn,攒一些新增经验后再learn,提高效率
MEMORY_SIZE = 2048    # replay memory的大小,越大越占用内存
MEMORY_WARMUP_SIZE = 512  # replay_memory 里需要预存一些经验数据,再开启训练
BATCH_SIZE = 128   # 每次给agent learn的数据数量,从replay memory随机里sample一批数据出来
#经验回放
class ReplayMemory(object):
    def __init__(self, max_size):
        self.buffer = collections.deque(maxlen=max_size)

    # 增加一条经验到经验池中
    def append(self, exp):
        self.buffer.append(exp)

    # 从经验池中选取N条经验出来
    def sample(self, batch_size):
        mini_batch = random.sample(self.buffer, batch_size)
        obs_batch, action_batch, reward_batch, next_obs_batch, done_batch = [], [], [], [], []

        for experience in mini_batch:
            s, a, r, s_p, done = experience
            obs_batch.append(s)
            action_batch.append(a)
            reward_batch.append(r)
            next_obs_batch.append(s_p)
            done_batch.append(done)

        return np.array(obs_batch).astype('float32'), \
            np.array(action_batch).astype('int64'), np.array(reward_batch).astype('float32'),\
            np.array(next_obs_batch).astype('float32'), np.array(done_batch).astype('float32')

    def __len__(self):
        return len(self.buffer)
from torch.utils.data import IterableDataset,DataLoader  
class MyDataset(IterableDataset):
    def __init__(self,env,agent,rpm,stage='train',size=200):
        self.env = env
        self.agent = agent 
        self.rpm = rpm if stage=='train' else None
        self.stage = stage
        self.size = size 
        
    def __iter__(self):
        obs,info = self.env.reset() # 重置环境, 重新开一局(即开始新的一个episode)
        step = 0
        batch_reward_true = [] #记录真实的reward
        while True:
            step += 1
            action = self.agent.sample(obs) 
            next_obs, reward, done, _, _ = self.env.step(action) # 与环境进行一个交互
            batch_reward_true.append(reward)
            
            if self.stage=='train':
                self.rpm.append((obs, action, reward, next_obs, float(done)))
                if (len(rpm) > MEMORY_WARMUP_SIZE) and (step % LEARN_FREQ == 0):
                    #yield batch_obs, batch_action, batch_reward, batch_next_obs,batch_done
                    yield self.rpm.sample(BATCH_SIZE),sum(batch_reward_true)
                    batch_reward_true.clear()
            
            else:
                obs_batch = np.array([obs]).astype('float32')
                action_batch = np.array([action]).astype('int64')
                reward_batch = np.array([reward]).astype('float32')
                next_obs_batch = np.array([next_obs]).astype('float32')
                done_batch = np.array([float(done)]).astype('float32')
                batch_data = obs_batch,action_batch,reward_batch,next_obs_batch,done_batch
                yield batch_data,sum(batch_reward_true)
                batch_reward_true.clear()
            
    
            if self.stage =='train':
                next_action = self.agent.sample(next_obs) # 训练阶段使用探索策略
            else:
                next_action = self.agent.predict(next_obs) # 验证阶段使用模型预测结果
 
            action = next_action
            obs = next_obs   

            if done:
                if self.stage=='train' and len(self.rpm)<MEMORY_WARMUP_SIZE: #确保训练一次
                    yield self.rpm.sample(len(self.rpm)),sum(batch_reward_true)
                    batch_reward_true.clear()
                    break
                else:
                    break
    def __len__(self):
        return self.size 
    

env = gym.make('CartPole-v1') 
rpm = ReplayMemory(MEMORY_SIZE)

ds_train = MyDataset(env,agent,rpm,stage='train',size=1000)
ds_val = MyDataset(env,agent,rpm,stage='val',size=200)
#ReplayMemory预存数据
while len(ds_train.rpm)<MEMORY_WARMUP_SIZE:
    for data in ds_train:
        print(len(ds_train.rpm))
13
47
167
272
511
521
def collate_fn(batch):
    samples,rewards = [x[0] for x in batch],[x[-1] for x in batch] 
    samples = [torch.from_numpy(np.concatenate([x[j] for x in samples])) for j in range(5)] 
    rewards = torch.from_numpy(np.array([sum(rewards)]).astype('float32'))
    return samples,rewards 

dl_train = DataLoader(ds_train,batch_size=1,collate_fn=collate_fn)
dl_val = DataLoader(ds_val,batch_size=1,collate_fn=collate_fn)
for batch in dl_train:
    break
import sys,datetime
from tqdm import tqdm
import numpy as np
from accelerate import Accelerator
from torchkeras import KerasModel
import pandas as pd 

from copy import deepcopy

class StepRunner:
    def __init__(self, net, loss_fn, accelerator=None, stage = "train", metrics_dict = None, 
                 optimizer = None, lr_scheduler = None
                 ):
        self.net,self.loss_fn,self.metrics_dict,self.stage = net,loss_fn,metrics_dict,stage
        self.optimizer,self.lr_scheduler = optimizer,lr_scheduler
        self.accelerator = accelerator if accelerator is not None else Accelerator()
    
    def __call__(self, batch):
        
        samples,reward = batch
        #torch_data = ([torch.from_numpy(x) for x in batch_data])
        loss = self.net.compute_loss(*samples)
        
        #backward()
        if self.optimizer is not None and self.stage=="train":
            self.accelerator.backward(loss)
            if self.accelerator.sync_gradients:
                self.accelerator.clip_grad_norm_(self.net.parameters(), 1.0)
            self.optimizer.step()
            if self.lr_scheduler is not None:
                self.lr_scheduler.step()
            self.optimizer.zero_grad()
                
            
        #losses (or plain metric)
        step_losses = {self.stage+'_reward':reward.item(), 
                       self.stage+'_loss':loss.item()}
        
        #metrics (stateful metric)
        step_metrics = {}
        if self.stage=="train":
            if self.optimizer is not None:
                step_metrics['lr'] = self.optimizer.state_dict()['param_groups'][0]['lr']
            else:
                step_metrics['lr'] = 0.0
        return step_losses,step_metrics
    

class EpochRunner:
    def __init__(self,steprunner,quiet=False):
        self.steprunner = steprunner
        self.stage = steprunner.stage
        self.accelerator = steprunner.accelerator
        self.net = steprunner.net
        self.quiet = quiet
        
    def __call__(self,dataloader):
        dataloader.agent = self.net 
        n = dataloader.size  if hasattr(dataloader,'size') else len(dataloader)
        loop = tqdm(enumerate(dataloader,start=1), 
                    total=n,
                    file=sys.stdout,
                    disable=not self.accelerator.is_local_main_process or self.quiet,
                    ncols=100
                   )
        epoch_losses = {}
        for step, batch in loop: 
            if step<n:
                step_losses,step_metrics = self.steprunner(batch)   
                step_log = dict(step_losses,**step_metrics)
                for k,v in step_losses.items():
                    epoch_losses[k] = epoch_losses.get(k,0.0)+v
                loop.set_postfix(**step_log) 
            else:
                break
            
        epoch_metrics = step_metrics
        epoch_metrics.update({self.stage+"_"+name:metric_fn.compute().item() 
                         for name,metric_fn in self.steprunner.metrics_dict.items()})
        epoch_losses = {k:v for k,v in epoch_losses.items()}
        epoch_log = dict(epoch_losses,**epoch_metrics)
        loop.set_postfix(**epoch_log)

        for name,metric_fn in self.steprunner.metrics_dict.items():
            metric_fn.reset()
            
        return epoch_log
    
KerasModel.StepRunner = StepRunner
KerasModel.EpochRunner = EpochRunner
keras_model = KerasModel(net= agent,loss_fn=None,
        optimizer=torch.optim.Adam(agent.model.parameters(),lr=1e-2))

dfhistory = keras_model.fit(train_data = dl_train,
    val_data=dl_val,
    epochs=600,
    ckpt_path='checkpoint.pt',
    patience=100,
    monitor='val_reward',
    mode='max',
    callbacks=None,
    plot= True,
    cpu=True)

3aad2955bb0a7f63ec3220feeb1dd432.gif

四,评估Agent

# 评估 agent, 跑 3 次,总reward求平均
def evaluate(env, agent, render=False):
    eval_reward = []
    for i in range(2):
        obs,info = env.reset()
        episode_reward = 0
        step=0
        while step<300:
            action = agent.predict(obs)  # 预测动作,只选最优动作
            obs, reward, done, _, _ = env.step(action)
            episode_reward += reward
            if render:
                show_state(env,step,info='reward='+str(episode_reward))
            if done:
                break
            step+=1
        eval_reward.append(episode_reward)
    return np.mean(eval_reward)
#直观显示动画
env = gym.make('CartPole-v1',render_mode="rgb_array") 

evaluate(env, agent, render=True)

4b454eda96489d798e15c51ff1a1a60c.gif可以看到,训练完成之后,我们的agent已经变得非常的智能了,能够维持倒立摆的平衡超过200s。🤗

288.5

五,保存Agent

torch.save(agent.state_dict(),'dqn_agent.pt')

万水千山总是情,点个在看行不行?🤗🤗

abfb51255c9799ef2b6dd28b33857442.png

本文notebook源码,以及更多有趣范例,可在公众号算法美食屋后台回复关键词:torchkeras,获取~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/666344.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

金九银十预备秋招: 大厂面试必考点及 Java 面试框架知识点整理

Java 面试 “金九银十”这个字眼对于程序员应该是再熟悉不过的了&#xff0c;每年的金九银十都会有很多程序员找工作、跳槽等一系列的安排。说实话&#xff0c;面试中 7 分靠能力&#xff0c;3 分靠技能&#xff1b;在刚开始的时候介绍项目都是技能中的重中之重&#xff0c;它…

龙膜公益“聚光行动”再起航 为云南山区小学援建绿色电脑教室

中国&#xff0c;上海——近日&#xff0c;全球汽车膜品牌龙膜的公益活动“为山区学校援建绿色电脑教室”在云南泸西县再度起航。为当地的“阿盈里小学”和“歹鲁小学”添置了2间电脑教室&#xff0c;配备了82台再生电脑&#xff0c;为600多名学生提供了数字化设备的使用机会&a…

Android Hilt:强大的依赖注入框架,高级传参解个耦?

作者&#xff1a;J船长 一、Hilt 干依赖注入的 Hilt是干嘛的 Hilt&#xff0c;一个依赖注入框架&#xff0c;谷歌搞出来的&#xff0c;基于Dagger&#xff0c;但是更加简洁易用。 什么是依赖注入 &#xff08;Dependency Injection&#xff09; 依赖注入是一种设计模式。主…

高校劳动积分小程序/基于微信小程序的高校劳动积分系统

摘 要 随着信息技术互联网和小程序的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的微信小程序应运而生&#xff0c;各行各业相继进入信息管理…

系统架构设计师-系统工程与信息系统基础(1)

一、系统工程概念 【系统工程】是一种组织管理技术。 【系统工程】是为了更好的实现系统的目的&#xff0c;对系统的组成要素、组成结构、信息流、控制机构进行分析研究的科学方法。 【系统工程】从整体出发、从系统观念出发、以求【整体最优】 【系统工程】利用计算机作为…

开放式耳机会不会吵到别人?2023年开放式耳机科普!

在了解开放式耳机会不会吵到别人之前&#xff0c;我们先了解下开放式耳机的基本知识&#xff01; 开放式耳机是一种不入耳&#xff0c;没有封闭耳朵的蓝牙耳机&#xff0c;可以听歌的同时接收来自外界声音&#xff0c;安全性高&#xff0c;也减少长期佩戴耳机带来的负担&#…

2023火爆的11门编程语言

2023火爆的11门编程语言 对于我个人来说没有语言偏好&#xff0c;根据不同的应用领域和需求&#xff0c;不同的编程语言都有其独特的优势和适用性。无论使用何种语言只要能更好的实现需求&#xff0c;解决痛点问题&#xff0c;就是好语言。 那么各种语言应用的场景解决了哪些…

项目管理专业人员能力评价CSPM与项目管理PMP对应关系

2021年10月&#xff0c;中共中央、国务院发布的《国家标准化发展纲要》明确提出构建多层次从业人员培养培训体系&#xff0c;开展专业人才培养培训和国家质量基础设施综合教育。建立健全人才的职业能力评价和激励机制。由中国标准化协会&#xff08;CAS&#xff09;组织开展的项…

Solon 成为信通院可信开源社区、可信开源项目

自2021年9月17日成立以来&#xff0c;可信开源社区共同体共有五批新成员加入。在4月21日“OSCAR开源生态建设论坛”上&#xff0c;可信开源社区共同体又迎来2位正式成员和6位预备成员&#xff0c;Solon 为其一&#xff08;图之右下角&#xff09;&#xff1a; 图片引用自CAICT可…

开放式耳机和封闭式耳机的区别?开放式耳机到底有哪些优缺点?

开放式耳机从字面意思可以理解为&#xff1a;开放耳朵&#xff0c;不需要入耳就可以听见声音的耳机&#xff0c;所以它和封闭式耳机的最大区别就是不入耳。这种耳机最大的优点就是不压迫不封闭耳道&#xff0c;而且在听耳机音的同时能够及时注意到周围环境的声音&#xff0c;从…

轻量级日志系统Loki——安装配置

关注“云计算就该这么学”微信公众号&#xff0c;搜索“001”&#xff0c;即可获取氪肝整理的154页Linux学习笔记。 Loki对标EFK/ELK&#xff0c;由于其轻量的设计&#xff0c;备受欢迎&#xff0c;Loki相比EFK/ELK&#xff0c;它不对原始日志进行索引&#xff0c;只对日志的标…

《主责数据保护与流动安全监管框架》重磅发布,美创以“产业研究力量”深入参与

历时四年&#xff0c;聚合行业安全专家智慧&#xff0c;凝炼行业安全最佳实践&#xff0c;数字时代&#xff1a;基于行业最佳实践的《主责数据保护与流动安全监管框架》&#xff08;以下简称“框架”&#xff09;于2023年6月17日第三届数字安全大会上正式发布。 该框架是在中国…

基于SpringBoot+Html的前后端分离的学习平台

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 在知识大爆炸的现代,怎…

一.Elasticsearch快速入门及使用

Elasticsearch快速入门及使用 一.Elasticsearch是什么二.基本概念1.index (索引)2. type (类型)3.Document (文档) 三.为什么Elasticsearch可以从海量数据里快速检索出数据四.Elasticsearch安装1.解压2.运行3.显示以下内容就是启动成功14.Kibana可视化软件安装 五.入门(基本的操…

代码浅析Point-LIO

0. 简介 对于最近出来的Point-LIO(鲁棒高带宽激光惯性里程计)&#xff0c;本人还是非常该兴趣的&#xff0c;为此花了一些时间重点分析了Point-LIO的代码&#xff0c;并研究了它相较于Fast-LIO2的区别 1. laserMapping.cpp 第一部分就是实现对激光雷达视场角的图像分割。首先…

Day01 项目简介分布式基础概念

最近在改进公司开发的商城项目&#xff0c;看到了尚硅谷的谷粒商城&#xff0c;就快速学习了下&#xff0c;因为之前的Kafka,Redis都是在这学习的&#xff0c;还有大数据的Flink。所以感觉一定不错&#xff0c;就开始了。 这里做一下学习笔记 一、项目简介 1 、项目背景 1 &…

UIAutomatorViewer工具的使用

目录 前言&#xff1a; 一、uiautomatorviewer介绍 二、启动uiautomatorviewer 三、uiautomatorviewer界面 四、连接手机 前言&#xff1a; UIAutomatorViewer是Android SDK提供的一个可视化界面分析工具&#xff0c;可以用于查看Android应用的UI界面结构、属性信息以及布…

元宇宙在技术大爆炸时代迎来链游新世界

元宇宙是从虚拟游戏、虚拟艺术收藏品开始兴起&#xff0c;然后逐步扩展到社交和金融领域的。元宇宙的终极形态就是一种“无限游戏”&#xff0c;也即打破边界、颠覆规则、不断迭代和进化发展的新世界。 政策落地&#xff0c;元宇宙未来才能充满潜力 2021 年以来&#xff0c;元…

SSM在线学习平台-计算机毕设 附源码85204

SSM在线学习平台 摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;在线学习平台当然也不能排除在外。在线学习平台是以实际运用为开发背景&#xff0c;运用软件工程原理和…

数字非洲,沐光而行

“华为是什么公司&#xff1f;我们不相信中国企业能有先进的通信技术&#xff01;你们也不要总是来找我&#xff01;”1998年&#xff0c;华为人刚踏上非洲所遇到的&#xff0c;不是来自阳光大陆的热情&#xff0c;而是来自刚果&#xff08;金&#xff09;客户冷冰冰的拒绝。 2…