棋类游戏中的智能决策 ——蒙特卡洛树搜索(MCTS)算法解析
在人工智能领域,蒙特卡洛树搜索(Monte Carlo Tree Search,MCTS)是一种强大的决策制定算法,尤其适用于复杂的游戏和规划问题。本文将深入浅出地介绍MCTS算法的原理、实现步骤,并通过井字游戏的实例来展示其应用。
一、背景知识
在棋类游戏中,人工智能的目标是找到最优策略以击败对手。传统的 Minimax 算法在处理高分支因子的游戏如围棋时表现不佳,而蒙特卡洛树搜索(MCTS)凭借其独特的优势脱颖而出。MCTS 不需要像 Minimax 那样遍历整个游戏树,而是通过随机模拟和统计分析,集中精力探索最有希望的分支,从而在复杂的棋类游戏中实现有效的决策。
1.1 MCTS算法简介
MCTS是一种基于采样的搜索算法,它结合了蒙特卡洛方法和树搜索策略,能够在大规模的决策空间中有效地寻找最优解。其核心思想是通过随机模拟来估计每个可选行动的价值,并利用这些信息逐步构建搜索树,以指导未来的决策。
1.2 应用场景
MCTS在众多领域都有广泛应用,特别是在棋类游戏、机器人控制和自动规划等领域表现出色。例如,AlphaGo利用MCTS在围棋中击败了世界冠军,展示了其强大的决策能力。
二、MCTS算法原理
2.1 核心思想
MCTS 算法的核心在于构建一个搜索树来表示游戏状态的可能演变。树中的每个节点代表特定的棋盘位置,叶子节点代表终端游戏状态(胜负或平局)。算法通过不断地迭代,每次迭代包括四个主要步骤:选择、扩展、模拟和反向传播。
2.2 算法步骤
2.2.1 选择(Selection)
从根节点开始,依据一定策略(如UCT公式)选择最有潜力的子节点,直到达到一个尚未完全展开的节点。UCT(Upper Confidence Bound for Trees)公式如下:
U C T = W i N i + C ln N p N i UCT = \frac{W_i}{N_i} + C \sqrt{\frac{\ln N_p}{N_i}} UCT=NiWi+CNilnNp
其中, W i W_i Wi 是节点i的胜利次数, N i N_i Ni 是节点i的访问次数, N p N_p Np 是父节点的访问次数,C是探索参数。
2.2.2 扩展(Expansion)
在未完全展开的节点处添加一个新的子节点,代表一个可能的动作。
2.2.3 模拟(Simulation)
从新扩展的节点开始,进行一次随机的游戏模拟,直到游戏结束,以此估计该路径的结果。
2.2.4 反向传播(Backpropagation)
根据模拟结果更新沿途所有节点的信息,包括访问次数和胜利次数等统计数据。
三、代码实现与应用
3.1 井字游戏中的MCTS实现
以下是一个使用Python实现的井字游戏MCTS算法示例:
import math
import random
from copy import deepcopyclass GameState:def __init__(self, board=None, player=1):self.board = board or [0] * 9 # 空格:0, X:1, O:-1self.player = player # 当前玩家 (1 表示 X, -1 表示 O)def get_possible_moves(self):return [i for i, v in enumerate(self.board) if v == 0]def make_move(self, move):new_board = self.board.copy()new_board[move] = self.playerreturn GameState(new_board, -self.player)def is_terminal(self):return self.get_winner() is not None or not self.get_possible_moves()def get_winner(self):winning_combinations = [(0,1,2), (3,4,5), (6,7,8), # 横向(0,3,6), (1,4,7), (2,5,8), # 纵向(0,4,8), (2,4,6) # 对角线]for combo in winning_combinations:total = sum(self.board[i] for i in combo)if total == 3:return 1 # X 胜if total == -3:return -1 # O 胜return None # 无胜者class Node:def __init__(self, state, parent=None, move=None):self.state = stateself.parent = parentself.children = {} # key: move, value: Nodeself.visits = 0self.wins = 0self.move = move # 导致该节点的动作def is_fully_expanded(self):return len(self.children) == len(self.state.get_possible_moves())def best_child(self, c_param=1.4):choices_weights = [(child.wins / child.visits + c_param * math.sqrt(math.log(self.visits) / child.visits), child)for child in self.children.values() if child.visits > 0]return max(choices_weights, key=lambda x: x[0])[1]def expand(self):tried_moves = set(self.children.keys())possible_moves = set(self.state.get_possible_moves()) - tried_movesmove = random.choice(list(possible_moves))new_state = self.state.make_move(move)child_node = Node(new_state, self, move)self.children[move] = child_nodereturn child_nodedef tree_policy(node):while not node.state.is_terminal():if not node.is_fully_expanded():return node.expand()else:node = node.best_child()return nodedef default_policy(state):current_state = deepcopy(state)while not current_state.is_terminal():possible_moves = current_state.get_possible_moves()move = random.choice(possible_moves)current_state = current_state.make_move(move)winner = current_state.get_winner()if winner == 1:return 1elif winner == -1:return 0else: # 平局return 0def backup(node, result):while node is not None:node.visits += 1if result == 1: # 当前玩家胜利node.wins += 1elif result == 0: # 平局或对方玩家胜利node.wins += 0node = node.parentdef mcts(root_state, iterations=1000):root_node = Node(root_state)for _ in range(iterations):leaf = tree_policy(root_node)simulation_result = default_policy(leaf.state)backup(leaf, simulation_result)return root_node.best_child(c_param=0).move # 最佳移动# 示例:使用MCTS进行一次井字游戏决策
if __name__ == "__main__":initial_state = GameState()best_move = mcts(initial_state, iterations=1000)print(f"推荐的最佳移动位置: {best_move}")
3.2 代码解释
- GameState类:用于表示游戏状态,包括获取可能的移动、执行移动、判断游戏是否结束以及获取胜者等方法。
- Node类:表示搜索树中的节点,包含游戏状态、父节点、子节点、访问次数和胜利次数等属性。
- tree_policy函数:实现选择和扩展步骤,从根节点开始选择最有潜力的子节点,直到达到未完全展开的节点。
- default_policy函数:实现模拟步骤,从当前状态开始进行随机模拟,直到游戏结束。
- backup函数:实现反向传播步骤,根据模拟结果更新节点的访问次数和胜利次数。
- mcts函数:主函数,执行多次迭代,每次迭代调用上述步骤,最终返回最佳移动。
四、可视化增强
为了更好地展示MCTS算法在井字游戏中的应用,可以添加棋盘的可视化功能。使用matplotlib库来绘制棋盘,以便更直观地观察游戏状态的变化。
import math
import random
from copy import deepcopy
import matplotlib.pyplot as plt
import numpy as np# 解决中文显示问题(如需要)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = Falseclass GameState:def __init__(self, board=None, player=1):self.board = board or [0] * 9 # 空格:0, X:1, O:-1self.player = player # 当前玩家 (1 表示 X, -1 表示 O)def get_possible_moves(self):return [i for i, v in enumerate(self.board) if v == 0]def make_move(self, move):new_board = self.board.copy()new_board[move] = self.playerreturn GameState(new_board, -self.player)def is_terminal(self):return self.get_winner() is not None or not self.get_possible_moves()def get_winner(self):winning_combinations = [(0,1,2), (3,4,5), (6,7,8), # 横向(0,3,6), (1,4,7), (2,5,8), # 纵向(0,4,8), (2,4,6) # 对角线]for combo in winning_combinations:total = sum(self.board[i] for i in combo)if total == 3:return 1 # X 胜if total == -3:return -1 # O 胜return None # 无胜者def print_board(self):symbols = {1: 'X', -1: 'O', 0: ' '}board_str = f" {symbols[self.board[0]]} | {symbols[self.board[1]]} | {symbols[self.board[2]]} \n"board_str += "-----------\n"board_str += f" {symbols[self.board[3]]} | {symbols[self.board[4]]} | {symbols[self.board[5]]} \n"board_str += "-----------\n"board_str += f" {symbols[self.board[6]]} | {symbols[self.board[7]]} | {symbols[self.board[8]]} \n"return board_strclass Node:def __init__(self, state, parent=None, move=None):self.state = stateself.parent = parentself.children = {} # key: move, value: Nodeself.visits = 0self.wins = 0self.move = move # 导致该节点的动作def is_fully_expanded(self):return len(self.children) == len(self.state.get_possible_moves())def best_child(self, c_param=1.4):choices_weights = [(child.wins / child.visits + c_param * math.sqrt(math.log(self.visits) / child.visits), child)for child in self.children.values() if child.visits > 0]return max(choices_weights, key=lambda x: x[0])[1]def expand(self):tried_moves = set(self.children.keys())possible_moves = set(self.state.get_possible_moves()) - tried_movesmove = random.choice(list(possible_moves))new_state = self.state.make_move(move)child_node = Node(new_state, self, move)self.children[move] = child_nodereturn child_nodedef tree_policy(node):while not node.state.is_terminal():if not node.is_fully_expanded():return node.expand()else:node = node.best_child()return nodedef default_policy(state):current_state = deepcopy(state)while not current_state.is_terminal():possible_moves = current_state.get_possible_moves()move = random.choice(possible_moves)current_state = current_state.make_move(move)winner = current_state.get_winner()if winner == 1:return 1elif winner == -1:return 0else: # 平局return 0def backup(node, result):while node is not None:node.visits += 1if result == 1: # 当前玩家胜利node.wins += 1elif result == 0: # 平局或对方玩家胜利node.wins += 0node = node.parentdef mcts(root_state, iterations=1000):root_node = Node(root_state)for _ in range(iterations):leaf = tree_policy(root_node)simulation_result = default_policy(leaf.state)backup(leaf, simulation_result)return root_node.best_child(c_param=0).move # 最佳移动def visualize_board(state):board = np.array(state.board).reshape(3, 3)plt.figure(figsize=(4, 4))plt.imshow(board, cmap='GnBu', alpha=0.3)for i in range(3):for j in range(3):cell_value = state.board[i*3 + j]if cell_value == 1:plt.text(j, i, 'X', ha='center', va='center', fontsize=30)elif cell_value == -1:plt.text(j, i, 'O', ha='center', va='center', fontsize=30)else:plt.text(j, i, '-', ha='center', va='center', fontsize=30)plt.title('井字游戏棋盘')plt.axis('off')plt.show()def play_game():initial_state = GameState()current_state = initial_statemove_history = []while not current_state.is_terminal():print(current_state.print_board())visualize_board(current_state)if current_state.player == 1:best_move = mcts(current_state, iterations=1000)move_history.append(best_move)current_state = current_state.make_move(best_move)print(f"MCTS AI选择了位置:{best_move}")else:move = int(input("请输入你的移动位置 (0-8): "))while move not in current_state.get_possible_moves():move = int(input("无效的移动,请重新输入 (0-8): "))move_history.append(move)current_state = current_state.make_move(move)print(current_state.print_board())visualize_board(current_state)winner = current_state.get_winner()if winner == 1:print("X 获胜!")elif winner == -1:print("O 获胜!")else:print("平局!")# 运行游戏
play_game()
代码解释
-
GameState 类:
- 用于表示游戏状态。
get_possible_moves()
返回当前状态下所有可能的移动位置。make_move(move)
根据移动生成新的游戏状态。is_terminal()
检查游戏是否结束。get_winner()
返回游戏的胜者,可能是 1(X 胜)、-1(O 胜)或 None(平局)。print_board()
以文本形式打印棋盘。
-
Node 类:
- 用于表示 MCTS 中的节点。
is_fully_expanded()
检查节点是否已完全展开。best_child()
根据 UCB1 公式选择最佳子节点。expand()
展开节点,生成新的子节点。
-
tree_policy(node):
- 从给定的节点开始,根据 MCTS 的选择和扩展策略,向下遍历树直到叶子节点。
-
default_policy(state):
- 从给定状态开始进行随机模拟,直到游戏结束,返回模拟结果。
-
backup(node, result):
- 将模拟结果反向传播到所有相关节点,更新节点的访问次数和胜利次数。
-
mcts(root_state, iterations):
- 根据 MCTS 算法,对给定的初始状态进行多次迭代,返回最佳移动。
-
visualize_board(state):
- 使用 matplotlib 可视化当前游戏状态的棋盘。
-
play_game():
- 允许玩家与 MCTS 算法对战,交替进行移动,直到游戏结束。
流程图
如何运行
- 确保已安装 Python 和必要的库(matplotlib, numpy)。
- 将上述代码复制到一个 Python 文件中(例如
mcts_tic_tac_toe.py
)。 - 运行该文件:
python mcts_tic_tac_toe.py
。 - 按照提示输入你的移动位置(0-8),与 MCTS AI 对战。
五、MCTS的应用领域
Monte Carlo 方法中的蒙特卡洛树搜索(Monte Carlo Tree Search,MCTS)是一种结合了随机采样和决策树搜索的算法,在许多领域都有广泛应用,以下是一些主要的应用场景:
游戏领域
- 棋类游戏 :在国际象棋、围棋、五子棋等棋类游戏中,MCTS 可帮助计算机 AI 分析各种可能的走法及其后续 developments,从而选择最优的下一步行动。例如,AlphaGo 就使用了 MCTS 在围棋中击败世界冠军。
- 即时战略游戏 :在一些即时战略游戏中,MCTS 可用于游戏 AI 的决策,如资源分配、建筑建造、单位训练和战斗策略等,使 AI 能够根据当前游戏局势做出合理的决策,与人类玩家进行对抗。
- 角色扮演游戏 :可辅助 NPC 的行为决策,让 NPC 根据玩家的行为和游戏环境做出更智能、更符合逻辑的反应,如选择攻击目标、使用技能、逃跑或与玩家进行交互等,增强游戏的趣味性和挑战性。
自动驾驶领域
- 路径规划 :自动驾驶车辆可以使用 MCTS 来规划最佳路径,考虑不同的交通状况、道路条件和目的地等因素,实时调整行驶路线,以应对各种复杂的交通场景,确保行驶的安全性和高效性。
- 决策制定 :在面对突发情况或复杂路况时,如前方车辆突然刹车、行人横穿马路、路口拥堵等,MCTS 能够快速评估各种可能的应对措施及其潜在风险,帮助自动驾驶系统做出最优的决策,如紧急制动、变道避让或减速慢行等。
机器人领域
- 路径规划与导航 :帮助机器人在未知或动态环境中规划路径,避开障碍物,安全地到达目标位置。例如,在家庭服务机器人中,可用于规划清洁路径;在工业机器人中,可用于在车间内移动物料或执行任务。
- 任务规划与决策 :在机器人执行复杂任务时,如装配、搬运、救援等,MCTS 可根据当前任务状态和环境信息,为机器人生成合理的操作序列和决策策略,优化任务执行的顺序和方式,提高任务的成功率和效率。
- 多机器人协作 :在多机器人系统中,MCTS 可用于协调机器人的行为和任务分配,使机器人之间能够高效地协作,共同完成复杂的任务,如搜索与救援、环境监测、物流配送等。
金融投资领域
- 投资组合优化 :MCTS 可以通过模拟不同的市场情况和投资组合配置,帮助投资者评估各种投资策略的潜在收益和风险,从而构建出最优的投资组合,实现资产的合理配置和收益的最大化。
- 风险评估与管理 :在金融风险管理中,利用 MCTS 模拟各种风险因素的变化及其对投资组合或金融机构的影响,如市场风险、信用风险、流动性风险等,为风险评估和管理提供依据,制定相应的风险控制措施和应急预案。
物流与供应链领域
- 路线规划与调度 :为物流配送车辆规划最优的行驶路线和配送顺序,考虑交通状况、路况、配送时间窗口等因素,降低运输成本,提高配送效率。
- 库存管理与补货策略 :通过模拟不同的销售情况和库存变化,帮助企业管理库存水平,确定最佳的补货时间和补货数量,避免库存积压或缺货现象的发生,优化供应链的运作。
项目管理领域
- 项目调度与资源分配 :在项目管理中,用于制定项目的进度计划和资源分配方案,考虑任务的依赖关系、资源的可用性和限制条件等因素,合理安排项目任务的执行顺序和时间,优化资源的利用效率,确保项目按时完成。
- 风险管理 :通过模拟项目实施过程中可能出现的各种风险事件及其影响,评估项目风险的发生概率和严重程度,制定相应的风险应对措施和预案,降低项目风险,提高项目成功的可能性。
计算机视觉与图像处理领域
- 目标检测与识别 :可用于目标检测算法中的搜索策略优化,通过构建决策树来快速定位和识别图像中的目标物体,提高目标检测的准确性和效率。
- 图像分割与修复 :在图像分割任务中,帮助确定最佳的分割边界和区域划分,实现对图像内容的精确分割;在图像修复中,可自动填补图像中的缺失或损坏部分,生成更完整、更自然的图像。
自然语言处理领域
- 文本生成与语言模型训练 :在文本生成任务中,如机器翻译、文本摘要、故事生成等,MCTS 可用于指导生成过程中的词汇选择和句子结构生成,生成更符合语义和语法规范的文本内容。
- 语义理解与问答系统 :帮助问答系统更好地理解用户的问题意图和语义信息,通过构建决策树来分析问题的不同可能解读和答案路径,从而提供更准确、更相关的答案。
六、UCB1 和UCT 策略的对比
算法步骤(基于 UCB1)
- 选择 :从根节点开始,基于某种策略(如 UCB1 公式)递归选择子节点进行扩展,直到到达叶子节点或未完全展开的节点。UCB1 公式如下:
U C B 1 = X ˉ j + 2 C F 2 ln n n j UCB1 = \bar{X}_j + 2C_F\sqrt{\frac{2\ln n}{n_j}} UCB1=Xˉj+2CFnj2lnn
其中,(\bar{X}_j) 是该节点下所有节点的平均奖励,(C_p) 是探索常数(通常设置为 (1/\sqrt{2})),n 是父节点被访问的次数,(n_j) 是子节点 j 被访问的次数。
- 扩展 :如果选择的节点不是叶子节点,则对该节点进行扩展,并随机选择其一个未访问的子节点。
- 模拟 :从刚刚扩展的非终端节点开始进行模拟(或 rollout),直到游戏结束,以产生价值估计。通常,这是通过采取随机动作来完成的。
- 反向传播 :游戏结束后,将结果(胜负或平局)反向传播到此次迭代中遍历的所有节点,更新这些节点的访问次数和胜利次数。
MCTS 过程详解
以井字游戏为例,以下是 MCTS 算法的具体过程:
- 初始化 :创建根节点,代表当前的游戏状态。
- 选择阶段 :从根节点开始,使用 UCB1 公式选择最有希望的子节点。例如,假设当前有三个可能的移动位置,分别对应三个子节点。根据每个子节点的访问次数和胜利次数计算 UCB1 值,选择 UCB1 值最大的子节点进行扩展。
- 扩展阶段 :对该子节点进行扩展,生成其所有可能的子节点,每个子节点代表一个可能的移动后的新游戏状态。
- 模拟阶段 :从新生成的子节点开始,随机选择移动,直到游戏结束。假设模拟结果显示当前玩家获胜,则将胜利次数加 1,访问次数加 1。
- 反向传播阶段 :将模拟结果反向传播到路径上的所有节点,更新它们的访问次数和胜利次数。例如,从新生成的子节点开始,逐层向上更新父节点,直到根节点。
- 重复迭代 :重复上述步骤,直到达到预设的迭代次数。最终,选择根节点下访问次数最多的子节点对应的移动作为最佳移动。
可运行代码示例
以下是使用 Python 实现的井字游戏 MCTS 算法的代码示例:
import math
import random
from copy import deepcopyclass GameState:def __init__(self, board=None, player=1):self.board = board or [0] * 9 # 空格:0, X:1, O:-1self.player = player # 当前玩家 (1 表示 X, -1 表示 O)def get_possible_moves(self):return [i for i, v in enumerate(self.board) if v == 0]def make_move(self, move):new_board = self.board.copy()new_board[move] = self.playerreturn GameState(new_board, -self.player)def is_terminal(self):return self.get_winner() is not None or not self.get_possible_moves()def get_winner(self):winning_combinations = [(0,1,2), (3,4,5), (6,7,8), # 横向(0,3,6), (1,4,7), (2,5,8), # 纵向(0,4,8), (2,4,6) # 对角线]for combo in winning_combinations:total = sum(self.board[i] for i in combo)if total == 3:return 1 # X 赢if total == -3:return -1 # O 赢return None # 无胜者class Node:def __init__(self, state, parent=None, move=None):self.state = stateself.parent = parentself.children = {} # key: move, value: Nodeself.visits = 0self.wins = 0self.move = move # 导致该节点的动作def is_fully_expanded(self):return len(self.children) == len(self.state.get_possible_moves())def best_child(self, c_param=1.4):choices_weights = [(child.wins / child.visits + c_param * math.sqrt(math.log(self.visits) / child.visits), child)for child in self.children.values() if child.visits > 0]return max(choices_weights, key=lambda x: x[0])[1]def expand(self):tried_moves = set(self.children.keys())possible_moves = set(self.state.get_possible_moves()) - tried_movesmove = random.choice(list(possible_moves))new_state = self.state.make_move(move)child_node = Node(new_state, self, move)self.children[move] = child_nodereturn child_nodedef tree_policy(node):while not node.state.is_terminal():if not node.is_fully_expanded():return node.expand()else:node = node.best_child()return nodedef default_policy(state):current_state = deepcopy(state)while not current_state.is_terminal():possible_moves = current_state.get_possible_moves()move = random.choice(possible_moves)current_state = current_state.make_move(move)return current_state.get_winner()def backup(node, result):while node is not None:node.visits += 1node.wins += resultresult = -result # 切换玩家视角node = node.parentdef mcts(root_state, iterations=1000):root_node = Node(root_state)for _ in range(iterations):leaf = tree_policy(root_node)simulation_result = default_policy(leaf.state)backup(leaf, simulation_result)return root_node.best_child(c_param=0).move # 最佳移动# 示例:使用 MCTS 进行一次井字游戏决策
if __name__ == "__main__":initial_state = GameState()best_move = mcts(initial_state, iterations=1000)print(f"推荐的最佳移动位置: {best_move}")
图示
以下是 MCTS 算法在井字游戏中的执行流程图:
以下是关于UCB1和UCT公式的差异,以及策略在MCTS算法中的效果差异的分析:
UCB1和UCT的定义与公式差异
- UCB1:主要用于多臂赌博机问题,其公式为:
U C B 1 i ( t ) = x ˉ i + c 2 ln t n i UCB1_i(t) = \bar{x}_i + c \sqrt{\frac{2 \ln t}{n_i}} UCB1i(t)=xˉi+cni2lnt
其中, x ˉ i \bar{x}_i xˉi 是第 i i i 个动作的平均奖励, n i n_i ni 是第 i i i 个动作被执行的次数, t t t 是当前总迭代次数, c c c 是常数,用于调整探索与利用之间的平衡。 - UCT:是UCB1在树搜索中的扩展,其公式为:
U C T i ( t ) = Q i + C ln N N i UCT_i(t) = Q_i + C \sqrt{\frac{\ln N}{N_i}} UCTi(t)=Qi+CNilnN
其中, Q i Q_i Qi 是状态-动作对的平均奖励值, N N N 是父节点(状态)的访问次数, N i N_i Ni 是子节点(状态-动作对)的访问次数, C C C 是探索参数。
策略在MCTS算法中的效果差异
- UCB1:适用于简单的多臂赌博机问题,当面对的问题不需要构建深层级子节点时,可直接运用UCB1来挑选最优选项。它的优势在于能够快速收敛到最优解,并且具有较小的计算开销。
- UCT:在复杂的树形结构决策问题中表现更优,尤其适用于蒙特卡洛树搜索(MCTS)。UCT通过结合UCB1的思想,能够在树搜索中有效地平衡探索和利用。它在游戏等领域的复杂决策问题中表现出色,能够更高效地找到较优解路径。
小结
- 适用场景:UCB1更适合简单的多臂赌博机问题,而UCT更适合复杂的树形结构决策问题,如棋类游戏等。
- 收敛性:UCB1在简单的场景中收敛较快,而UCT在复杂的树搜索中能够更有效地找到较优解。
- 探索与利用的平衡:UCB1和UCT都通过调整参数来平衡探索与利用,但UCT在树搜索中需要考虑不同层级节点的关系,比UCB1更复杂。
七、总结
蒙特卡洛树搜索(MCTS)算法作为一种强大的决策制定工具,在解决复杂的游戏和规划问题中发挥着重要作用。通过本文的介绍,读者不仅了解了MCTS的基本原理和实现方法,还通过井字游戏的实例看到了其实际应用效果。希望这篇博客能够激发读者在其他领域应用MCTS算法的兴趣和实践。
八、参考资料
https://blog.csdn.net/DeepViewInsight/article/details/132959033
https://blog.csdn.net/weixin_42072959/article/details/144202081