还记得2016年3月AlphaGo与李世石的那场世纪对决吗?当时的第37手让全世界为之震惊——这一步看似错误的棋,后来却被誉为天才之举。这就是强化学习的魅力所在。

最近重看了《AlphaGo》纪录片,再次被这种学习方式深深打动。可怕的是,AlphaGo并没有从数据库、规则或策略书中学习棋艺,而是通过数百万次自我对弈,在实践中学会了如何获胜。

什么是强化学习?

观察婴儿学走路的过程:站起来,摔倒,再次尝试——最终迈出了第一步。

没有老师教他们如何做,婴儿完全是通过试错来学习走路的。当他们能够站立或走几步时,这对他们来说就是奖励。毕竟,他们的目标是能够走路。如果摔倒了,就没有奖励。

这种试错和奖励的学习过程,正是强化学习(RL)的基本思想。

强化学习是一种学习方法,智能体通过与环境交互来学习哪些行动能够带来奖励。

其目标是:长期获得尽可能多的奖励。

  • 与监督学习不同,这里没有"正确答案"或标签。智能体必须自己找出哪些决策是好的。
  • 与无监督学习不同,目标不是发现数据中的隐藏模式,而是执行那些能最大化奖励的行动。

强化学习智能体如何思考、决策和学习

要让强化学习智能体学习,需要四个要素:对当前位置的认知(状态)、可以执行的操作(动作)、想要达成的目标(奖励)以及过去策略的表现如何(价值)。

智能体行动,获得反馈,然后改进。

为了实现这一点,需要四个核心组件:

1. 策略/战略

这是智能体在特定状态下决定执行哪个动作的规则或策略。在简单情况下,这是一个查找表。在更复杂的应用中(例如使用神经网络),它是一个函数。

2. 奖励信号

奖励是来自环境的反馈。例如,胜利可以是+1,平局是0,失败是-1。智能体的目标是在尽可能多的步骤中收集尽可能多的奖励。

3. 价值函数

这个函数估计一个状态的预期未来奖励。奖励显示智能体该动作是"好"还是"坏"。价值函数估计一个状态有多好——不仅仅是当前,而是考虑智能体从该状态开始可以期待的未来奖励。因此,价值函数估计的是一个状态的长期利益。

4. 环境模型

模型告诉智能体:“如果我在状态S中执行动作A,我可能会到达状态S’并获得奖励R。”

不过,在Q-learning等无模型方法中,这并不是必需的。

利用与探索:第37手的启示

你可能还记得AlphaGo与李世石第二局中的第37手:

这是一个看起来像错误的不寻常走法——但后来被誉为天才之举。

算法为什么要这样做?

计算机程序在尝试新的东西。这叫做探索。

强化学习需要两者兼顾:智能体必须在利用和探索之间找到平衡。

  • 利用意味着智能体使用它已经知道的动作。
  • 探索则是智能体第一次尝试的动作。它尝试这些动作是因为它们可能比已知的动作更好。

智能体通过试错来寻找最优策略。

用强化学习玩井字棋

让我们通过一个大家都很熟悉的游戏来看看强化学习的实际应用。

你可能小时候也玩过:井字棋。

这个游戏非常适合作为入门例子,因为它不需要神经网络,规则清晰,我们只需要一点Python代码就能实现:

  • 我们的智能体从对游戏的零知识开始。它就像第一次看到这个游戏的人类一样。
  • 智能体逐渐评估每个游戏情况:0.5分意味着"我还不知道在这里是否会获胜",1.0意味着"这种情况几乎肯定会胜利"。
  • 通过玩很多局游戏,智能体观察什么有效——并调整其策略。

目标是:在每一轮中,智能体应该选择能带来最高长期奖励的行动。

在这一部分,我们将逐步构建这样一个强化学习系统,创建TicTacToeRL.py文件。

→ 你可以在这个GitHub仓库中找到所有代码。

1. 构建游戏环境

在强化学习中,智能体通过与环境的交互来学习。环境决定什么是状态(例如当前棋盘)、允许哪些动作(例如你可以在哪里下棋)以及对动作有什么反馈(例如获胜奖励+1)。

理论上,我们称这种设置为马尔可夫决策过程:模型由状态、动作和奖励组成。

首先,我们创建一个TicTacToe类。这管理游戏棋盘(我们创建为3×3的NumPy数组)并管理游戏逻辑:

  • reset(self)函数开始新游戏。
  • available_actions()函数返回所有空位。
  • step(self, action, player)函数执行游戏移动。这里我们返回新状态、奖励(1=获胜,0.5=平局,-10=无效移动)和游戏状态。在这个例子中,我们对无效移动给予-10的重罚,以便智能体快速学会避免它们——这是小型强化学习环境中的常见技巧。
  • check_winner()函数检查玩家是否连成三个X或O并因此获胜。
  • render_gui()我们用matplotlib将当前棋盘显示为X和O图形。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import numpy as np
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import random
from collections import defaultdict

# 井字棋游戏环境
class TicTacToe:
    def __init__(self):
        self.board = np.zeros((3, 3), dtype=int)
        self.done = False
        self.winner = None

    def reset(self):
        self.board[:] = 0
        self.done = False
        self.winner = None
        return self.get_state()

    def get_state(self):
        return tuple(self.board.flatten())

    def available_actions(self):
        return [(i, j) for i in range(3) for j in range(3) if self.board[i, j] == 0]

    def step(self, action, player):
        if self.done:
            raise ValueError("游戏已结束")

        i, j = action
        if self.board[i, j] != 0:
            return self.get_state(), -10, True

        self.board[i, j] = player
        if self.check_winner(player):
            self.done = True
            self.winner = player
            return self.get_state(), 1, True
        elif not self.available_actions():
            self.done = True
            return self.get_state(), 0.5, True

        return self.get_state(), 0, False

    def check_winner(self, player):
        for i in range(3):
            if all(self.board[i, :] == player) or all(self.board[:, i] == player):
                return True
        if all(np.diag(self.board) == player) or all(np.diag(np.fliplr(self.board)) == player):
            return True
        return False

    def render_gui(self):
        fig, ax = plt.subplots()
        ax.set_xticks([0.5, 1.5], minor=False)
        ax.set_yticks([0.5, 1.5], minor=False)
        ax.set_xticks([], minor=True)
        ax.set_yticks([], minor=True)
        ax.set_xlim(-0.5, 2.5)
        ax.set_ylim(-0.5, 2.5)
        ax.grid(True, which='major', color='black', linewidth=2)

        for i in range(3):
            for j in range(3):
                value = self.board[i, j]
                if value == 1:
                    ax.plot(j, 2 - i, 'x', markersize=20, markeredgewidth=2, color='blue')
                elif value == -1:
                    circle = plt.Circle((j, 2 - i), 0.3, fill=False, color='red', linewidth=2)
                    ax.add_patch(circle)

        ax.set_aspect('equal')
        plt.axis('off')
        plt.show()

2. 编程Q-learning智能体

接下来,我们定义学习部分:我们的智能体。

它决定在特定状态下执行哪个动作以获得尽可能多的奖励。

智能体使用经典的强化学习方法Q-learning。为每个状态和动作的组合存储一个Q值——该动作的估计长期收益。

最重要的方法是:

  • 使用choose_action(self, state, actions)函数,智能体在每个游戏情况下决定是选择它已经熟知的动作(利用)还是尝试尚未充分测试的新动作(探索)。

这个决策基于所谓的ε-贪婪方法: 以ε = 0.1的概率,智能体选择随机动作(探索), 以90%的概率(1 – ε),它基于Q表选择当前已知的最佳动作(利用)。

  • 通过update(state, action, reward, next_state, next_actions)函数,我们根据动作的好坏和后续发生的情况调整Q值。这是智能体的核心学习步骤。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Q-learning智能体
class QLearningAgent:
    def __init__(self, alpha=0.1, gamma=0.9, epsilon=0.1):
        self.q_table = defaultdict(float)
        self.alpha = alpha  # 学习率
        self.gamma = gamma  # 折扣因子
        self.epsilon = epsilon  # 探索率

    def get_q(self, state, action):
        return self.q_table[(state, action)]

    def choose_action(self, state, actions):
        if random.random() < self.epsilon:
            return random.choice(actions)
        else:
            q_values = [self.get_q(state, a) for a in actions]
            max_q = max(q_values)
            best_actions = [a for a, q in zip(actions, q_values) if q == max_q]
            return random.choice(best_actions)

    def update(self, state, action, reward, next_state, next_actions):
        max_q_next = max([self.get_q(next_state, a) for a in next_actions], default=0)
        old_value = self.q_table[(state, action)]
        new_value = old_value + self.alpha * (reward + self.gamma * max_q_next - old_value)
        self.q_table[(state, action)] = new_value

3. 训练智能体

实际的学习过程从这一步开始。在训练期间,智能体通过试错学习。智能体玩许多游戏,记住哪些动作效果好——并调整其策略。

在训练过程中,智能体学习其动作如何获得奖励,其行为如何影响后续状态以及长期更好的策略如何发展。

  • 通过train(agent, episodes=10000)函数,我们定义智能体对抗简单的随机对手玩10,000局游戏。 在每一局中,智能体(玩家1)先移动,然后是对手(玩家2)。每次移动后,智能体通过update()学习。
  • 每1000局游戏我们保存有多少胜利、平局和失败。
  • 最后,我们用matplotlib绘制学习曲线。它显示智能体如何随时间改进。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 训练与学习曲线
def train(agent, episodes=10000):
    env = TicTacToe()
    results = {"win": 0, "draw": 0, "loss": 0}

    win_rates = []
    draw_rates = []
    loss_rates = []

    for episode in range(episodes):
        state = env.reset()
        done = False

        while not done:
            actions = env.available_actions()
            action = agent.choose_action(state, actions)

            next_state, reward, done = env.step(action, player=1)

            if done:
                agent.update(state, action, reward, next_state, [])
                if reward == 1:
                    results["win"] += 1
                elif reward == 0.5:
                    results["draw"] += 1
                else:
                    results["loss"] += 1
                break

            opp_actions = env.available_actions()
            opp_action = random.choice(opp_actions)
            next_state2, reward2, done = env.step(opp_action, player=-1)

            if done:
                agent.update(state, action, -1 * reward2, next_state2, [])
                if reward2 == 1:
                    results["loss"] += 1
                elif reward2 == 0.5:
                    results["draw"] += 1
                else:
                    results["win"] += 1
                break

            next_actions = env.available_actions()
            agent.update(state, action, reward, next_state2, next_actions)
            state = next_state2

        if (episode + 1) % 1000 == 0:
            total = sum(results.values())
            win_rates.append(results["win"] / total)
            draw_rates.append(results["draw"] / total)
            loss_rates.append(results["loss"] / total)
            print(f"第{episode+1}局: 胜利 {results['win']}, 平局 {results['draw']}, 失败 {results['loss']}")
            results = {"win": 0, "draw": 0, "loss": 0}

    x = [i * 1000 for i in range(1, len(win_rates) + 1)]
    plt.plot(x, win_rates, label="胜率")
    plt.plot(x, draw_rates, label="平局率")
    plt.plot(x, loss_rates, label="败率")
    plt.xlabel("训练局数")
    plt.ylabel("比率")
    plt.title("Q-learning智能体学习曲线")
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.show()

4. 棋盘可视化

通过主程序if __name__ == "__main__":我们定义程序的起点。它确保当我们执行脚本时智能体的训练自动运行。我们使用render_gui()方法将井字棋棋盘显示为图形。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 主程序
if __name__ == "__main__":
    agent = QLearningAgent()
    train(agent, episodes=10000)

    # 示例棋盘可视化
    env = TicTacToe()
    env.board[0, 0] = 1
    env.board[1, 1] = -1
    env.render_gui()

终端执行

我们将代码保存为TicTacToeRL.py文件。

在终端中,我们导航到存储TicTacToeRL.py的相应目录,并使用命令python TicTacToeRL.py执行文件。

在终端中,我们可以看到每1000局后我们的智能体赢了多少局游戏,在可视化中我们看到学习曲线显示智能体的改进过程。

总结思考

通过井字棋,我们使用一个简单的游戏和一些Python代码——但我们可以清楚地看到强化学习是如何工作的:

  • 智能体从没有任何先验知识开始。
  • 它通过反馈和经验制定策略。
  • 因此,它的决策逐渐改善——不是因为它知道规则,而是因为它学习了。

在我们的例子中,对手是一个随机智能体。接下来,我们可以看看我们的Q-learning智能体如何对抗另一个学习智能体或对抗我们自己的表现。

强化学习向我们展示,机器智能不仅通过知识或信息创造——而是通过经验、反馈和适应。

进一步学习资源

强化学习的魅力在于,它让我们看到了机器如何像人类一样从错误中学习,并最终超越人类的表现。正如AlphaGo的第37手所证明的那样,有时候最好的策略就是勇于探索未知。