Hopfield网络实战:用Python从零构建你的第一个联想记忆模型

最近在整理一些老照片,发现不少照片因为时间久远,出现了斑点、划痕,甚至部分区域模糊不清。看着这些承载记忆的画面变得残缺,我就在想,有没有一种方法,能像我们的大脑一样,看到模糊的片段就能自动补全完整的场景?这让我想起了在神经网络领域一个经典而优雅的模型——Hopfield网络。它不像如今动辄千亿参数的大模型那样复杂,但其核心思想却异常迷人:用简单的规则,模拟记忆的存储与联想。

对于刚接触神经网络的朋友来说,Hopfield网络是一个绝佳的起点。它结构清晰,原理直观,代码实现不过百行,却能让你亲手搭建一个具备“记忆”和“联想”能力的系统。今天,我们就抛开复杂的数学推导,直接动手,用Python从零开始,实现一个基础的Hopfield网络。我们将用它来“记住”几个简单的图案,然后看看它如何从一个被“噪音”破坏的、残缺的输入中,神奇地“回忆”出原始图案。这个过程,就像教AI玩一个高级版的“看图猜物”游戏。

1. 理解核心:Hopfield网络如何“记忆”

在开始写代码之前,我们必须先搞懂Hopfield网络运作的“灵魂”。它不是一个用于分类或预测的前馈网络,而是一个动力系统。你可以把它想象成一个由许多小磁针(神经元)构成的网络,每个磁针只能指向上(+1)或下(-1)。这些小磁针之间通过弹簧(连接权重)相互拉扯,整个系统会自发地寻找一种最“舒服”、能量最低的稳定摆放方式。

1.1 网络的“记忆”是什么?

Hopfield网络的“记忆”,并不是像电脑硬盘那样存储原始数据。它存储的是模式之间的关联关系。具体来说,记忆被编码在神经元之间的连接权重里。

  • 神经元与状态:假设网络有N个神经元,每个神经元的状态 s_i 取值 +1(激活)或 -1(抑制)。一个特定的图案(比如一个3x3的黑白图标)可以拉平成一个由 +1-1 组成的N维向量,这就是一个“模式”。
  • 权重矩阵:连接权重 W 是一个 N x N 的矩阵,其中元素 w_ij 表示从神经元 j 到神经元 i 的连接强度。Hopfield网络有两个关键约束:
    1. 无自连接w_ii = 0,神经元不连接自己。
    2. 对称性w_ij = w_ji,连接是对称的。这保证了网络动态演化的稳定性。

注意:权重矩阵 W 就是网络的“长期记忆”。一旦通过训练确定下来,在回忆阶段就不会再改变。

1.2 “回忆”的过程:能量最小化

网络如何从模糊输入中找到正确记忆?这依赖于一个叫做能量函数的概念。对于给定的神经元状态向量 s,能量 E 定义为:

E = -0.5 * sum_i sum_j (w_ij * s_i * s_j)

这个公式的直观理解是:如果相连的神经元状态(s_is_j)与它们之间的连接权重(w_ij) “配合”得好(例如,两者都为正且权重为正,或一正一负且权重为负),那么它们对能量的贡献就是负的,从而降低总能量。网络会自发地朝着能量更低的状态演化。

回忆的本质:我们将一个残缺或带噪声的模式作为网络的初始状态。然后,网络根据一个简单的规则(下面会讲)异步地更新神经元的状态。每次更新,如果可能,都会使整个系统的能量降低或保持不变。最终,网络会稳定在某个能量局部最小值点,这个点对应的状态,就是它“回忆”起的模式。我们预先存储的记忆模式,就被设计成这些能量低谷。

1.3 状态更新规则:神经元的“决策”

网络中的神经元如何决定自己下一刻是 +1 还是 -1?它很“民主”,会听取所有其他邻居的意见。对于神经元 i,它计算一个加权和:

输入总和 h_i = sum_j (w_ij * s_j)

然后根据这个总和的正负来决定自己的新状态:

新状态 s_i_new = +1, 如果 h_i >= 0
               = -1, 如果 h_i < 0

在实际更新时,我们采用异步更新策略:每次随机挑选一个神经元,根据上述规则更新其状态。这种逐个更新的方式,能确保能量函数单调不增,最终稳定下来。

2. 从理论到代码:搭建网络骨架

理解了原理,我们就可以用Python来构建这个网络了。我们将创建一个 HopfieldNetwork 类,它主要包含两个核心方法:train 用于记忆模式,recall 用于回忆模式。

首先,我们初始化网络。只需要知道神经元的数量即可。

import numpy as np

class HopfieldNetwork:
    def __init__(self, n_neurons):
        """
        初始化Hopfield网络。
        参数:
            n_neurons (int): 网络中神经元的数量。
        """
        self.n_neurons = n_neurons
        # 初始化权重矩阵,设置为零矩阵。注意我们不需要偏置项。
        self.weights = np.zeros((n_neurons, n_neurons))
        print(f"初始化了一个包含 {n_neurons} 个神经元的Hopfield网络。")

接下来是实现训练(记忆) 的方法。我们将使用经典的赫布学习规则。假设我们有 P 个要记忆的模式,每个模式是一个长度为 N 的向量,元素为 +1-1。权重更新公式为: w_ij = (1/N) * sum_over_patterns (pattern_i * pattern_j), 其中 i != j

    def train(self, patterns):
        """
        使用赫布学习规则训练网络,存储给定的模式。
        参数:
            patterns (np.ndarray): 一个二维数组,形状为 (P, N),
                                   P是模式数量,N是神经元数量(需与初始化一致)。
                                  每个元素应为 +1 或 -1。
        """
        P, N = patterns.shape
        if N != self.n_neurons:
            raise ValueError(f"模式维度 {N} 与网络神经元数 {self.n_neurons} 不匹配!")

        # 重置权重矩阵
        self.weights = np.zeros((N, N))

        # 赫布学习规则
        for p in range(P):
            pattern = patterns[p].reshape(-1, 1)  # 转换为列向量 (N, 1)
            # 外积 pattern * pattern.T 得到一个 (N, N) 矩阵
            self.weights += np.dot(pattern, pattern.T)

        # 除以N,并确保对角线为0(无自连接)
        self.weights /= N
        np.fill_diagonal(self.weights, 0)

        # 由于权重是对称的,我们也可以显式地平均一下(非必须,但更严谨)
        # self.weights = (self.weights + self.weights.T) / 2
        # np.fill_diagonal(self.weights, 0)

        print(f"成功存储了 {P} 个模式。")
        # 可选:打印一些权重统计信息
        print(f"权重矩阵范围: [{self.weights.min():.3f}, {self.weights.max():.3f}]")

然后是回忆方法。给定一个初始状态(可能是有噪声的输入),网络将进行迭代,直到状态稳定或达到最大迭代次数。

    def recall(self, initial_state, max_iterations=100, verbose=False):
        """
        从初始状态开始,异步更新神经元状态,直到收敛或达到最大迭代次数。
        参数:
            initial_state (np.ndarray): 初始状态向量,形状 (N,),元素为 +1 或 -1。
            max_iterations (int): 最大更新迭代次数。
            verbose (bool): 如果为True,打印每次迭代的能量值。
        返回:
            np.ndarray: 收敛后的稳定状态向量。
            int: 实际迭代次数。
        """
        if len(initial_state) != self.n_neurons:
            raise ValueError("初始状态维度与网络不匹配!")

        # 复制初始状态,避免修改原数据
        current_state = initial_state.copy().astype(np.float64)
        # 记录能量变化(可选,用于观察收敛过程)
        energy_history = []

        for it in range(max_iterations):
            state_changed = False
            # 创建一个随机顺序的神经元索引,用于异步更新
            update_order = np.random.permutation(self.n_neurons)

            for idx in update_order:
                # 计算神经元idx的输入总和
                h_i = np.dot(self.weights[idx, :], current_state)
                # 根据输入总和决定新状态
                new_state = 1 if h_i >= 0 else -1
                # 如果状态发生变化
                if new_state != current_state[idx]:
                    current_state[idx] = new_state
                    state_changed = True

            # 计算当前能量(可选)
            energy = -0.5 * np.dot(current_state.T, np.dot(self.weights, current_state))
            energy_history.append(energy)

            if verbose and (it % 10 == 0 or it == max_iterations - 1):
                print(f"迭代 {it:3d}, 能量: {energy:.3f}")

            # 如果本轮迭代没有任何神经元状态改变,则认为已收敛
            if not state_changed:
                if verbose:
                    print(f"在第 {it} 次迭代后收敛。")
                return current_state.astype(np.int8), it

        if verbose:
            print(f"达到最大迭代次数 {max_iterations},未完全收敛。")
        return current_state.astype(np.int8), max_iterations

    def calculate_energy(self, state):
        """计算给定状态的能量值。"""
        return -0.5 * np.dot(state.T, np.dot(self.weights, state))

至此,一个功能完整的Hopfield网络类就构建好了。它包含了记忆(train)和联想(recall)的核心逻辑。接下来,我们就要用它来玩点有趣的。

3. 实战演练:让网络记住并识别字母

为了直观展示,我们设计一个简单的场景:让网络记住三个由 +1 (白色) 和 -1 (黑色) 构成的字母图案,比如 “X”, “O”, “T”。每个图案用 5x5 的网格表示,拉平后就是 25 维的向量。

3.1 定义记忆模式

我们先定义这三个模式。为了可视化,我们定义一个辅助函数来打印图案。

def pattern_to_grid(pattern, shape=(5,5)):
    """将一维向量转换为二维网格用于显示。"""
    return pattern.reshape(shape)

def print_pattern(pattern, shape=(5,5), char_on='■', char_off='□'):
    """用字符打印图案。"""
    grid = pattern_to_grid(pattern, shape)
    for row in grid:
        line = ''.join([char_on if x == 1 else char_off for x in row])
        print(line)
    print()

# 定义三个记忆模式:X, O, T
# 我们用1代表白色(■),-1代表黑色(□)
pattern_X = np.array([
    1, -1, -1, -1,  1,
    -1,  1, -1,  1, -1,
    -1, -1,  1, -1, -1,
    -1,  1, -1,  1, -1,
    1, -1, -1, -1,  1
])

pattern_O = np.array([
    -1,  1,  1,  1, -1,
     1, -1, -1, -1,  1,
     1, -1, -1, -1,  1,
     1, -1, -1, -1,  1,
    -1,  1,  1,  1, -1
])

pattern_T = np.array([
     1,  1,  1,  1,  1,
    -1, -1,  1, -1, -1,
    -1, -1,  1, -1, -1,
    -1, -1,  1, -1, -1,
    -1, -1,  1, -1, -1
])

patterns = np.vstack([pattern_X, pattern_O, pattern_T])
print("我们定义的三个记忆模式:")
titles = ['X', 'O', 'T']
for i, title in enumerate(titles):
    print(f"模式 '{title}':")
    print_pattern(patterns[i])

3.2 训练网络

现在,创建网络并训练它记住这三个模式。

# 初始化网络,神经元数量为25 (5x5)
n_neurons = 25
hn = HopfieldNetwork(n_neurons)

# 训练网络
hn.train(patterns)

训练完成后,权重矩阵 hn.weights 就包含了这三个模式的所有关联信息。我们可以简单查看一下权重的分布,它通常关于0对称。

3.3 测试回忆能力:处理噪声输入

最激动人心的部分来了。我们模拟一个被噪声破坏的“X”图案,看看网络能否将其恢复。

def add_noise(pattern, noise_level=0.3):
    """以一定概率随机翻转图案中的像素(将+1变为-1,反之亦然)。"""
    noisy_pattern = pattern.copy()
    flip_mask = np.random.random(pattern.shape) < noise_level
    noisy_pattern[flip_mask] *= -1  # 翻转
    return noisy_pattern

# 创建一个带噪声的“X”模式
noise_level = 0.4  # 40%的像素被翻转
noisy_X = add_noise(pattern_X, noise_level)

print("原始模式 'X':")
print_pattern(pattern_X)
print(f"添加了 {noise_level*100:.0f}% 噪声后的模式:")
print_pattern(noisy_X)

# 让网络回忆
print("开始回忆过程...")
recalled_state, iterations = hn.recall(noisy_X, max_iterations=200, verbose=True)

print(f"\n经过 {iterations} 次迭代后,回忆出的模式:")
print_pattern(recalled_state)

# 检查回忆结果是否与原始模式匹配
if np.array_equal(recalled_state, pattern_X):
    print("✅ 成功回忆出原始模式 'X'!")
elif np.array_equal(recalled_state, pattern_O):
    print("❌ 回忆成了模式 'O'。")
elif np.array_equal(recalled_state, pattern_T):
    print("❌ 回忆成了模式 'T'。")
else:
    print("⚠️  回忆到了一个未知的稳定状态(可能是伪状态)。")

运行这段代码,你很可能看到网络成功地将一个近半像素都出错的“X”修复回了原本清晰的样子。这个过程就像魔法一样:网络并没有存储原始的“X”图片,但它存储的关联规则,引导着系统从混乱走向了有序。

3.4 探索网络的容量与局限

Hopfield网络并非万能。一个经典的限制就是它的存储容量。理论研究表明,对于一个有N个神经元的网络,它能可靠存储的模式数量P大约在 0.15N 左右。对于我们这个25个神经元的网络,大约能存3-4个模式。如果我们试图存储太多模式,网络就会产生混淆,甚至回忆出一些从未存储过的“混合”模式(称为伪状态)。

让我们做个实验,尝试存储4个、5个模式,看看回忆成功率如何变化。

def test_capacity(network_size, num_patterns_to_store, trials_per_pattern=10, noise_level=0.2):
    """
    测试网络存储容量。
    参数:
        network_size (int): 神经元数量(假设为完美平方数,如25, 36, 64)。
        num_patterns_to_store (int): 尝试存储的模式数量。
        trials_per_pattern (int): 对每个存储的模式,进行回忆测试的次数。
        noise_level (float): 测试时添加的噪声水平。
    返回:
        float: 平均回忆成功率。
    """
    N = network_size
    P = num_patterns_to_store
    hn_test = HopfieldNetwork(N)

    # 随机生成P个随机的记忆模式
    random_patterns = np.random.choice([-1, 1], size=(P, N))
    hn_test.train(random_patterns)

    success_count = 0
    total_trials = P * trials_per_pattern

    for p_idx in range(P):
        original_pattern = random_patterns[p_idx]
        for _ in range(trials_per_pattern):
            noisy_pattern = add_noise(original_pattern, noise_level)
            recalled, _ = hn_test.recall(noisy_pattern, max_iterations=100, verbose=False)
            if np.array_equal(recalled, original_pattern):
                success_count += 1

    success_rate = success_count / total_trials
    return success_rate

# 测试不同存储数量下的成功率
print("测试网络容量(神经元数N=25):")
print("-" * 40)
print("存储模式数(P) | 近似理论容量(0.15N) | 实测成功率")
print("-" * 40)

for P in [2, 3, 4, 5, 6]:
    rate = test_capacity(network_size=25, num_patterns_to_store=P, trials_per_pattern=20)
    print(f"      {P:2d}      |        {0.15*25:.1f}        |    {rate:.1%}")

运行这个测试,你可能会发现,当P=3时,成功率接近100%;P=4时,成功率开始显著下降;P=5或6时,成功率可能变得很低。这直观地验证了网络的容量限制。

4. 深入原理与高级话题

通过上面的实战,我们已经体验了Hopfield网络的基本能力。但要想真正用好它,还需要理解其背后的数学和物理内涵,并了解它的现代演进。

4.1 能量景观可视化

Hopfield网络的行为可以形象地用一个“能量景观”来理解。每个可能的状态(25维空间中的一个点)都有一个能量值。记忆模式被设计在能量景观的“谷底”(局部最小值),而噪声输入则位于“山坡”上。网络的异步更新规则,就像让一个小球沿着最陡的下坡方向滚动,直到落入最近的谷底。

虽然我们无法可视化25维空间,但可以通过一个简化的2D或3D示意图来类比。理解这一点有助于明白为什么网络会陷入局部最小值(错误的记忆),以及为什么容量有限(谷底距离太近会合并)。

4.2 现代Hopfield网络的启示

传统的Hopfield网络由于容量等问题,在深度学习浪潮中一度被边缘化。但近年来,它以一种令人惊讶的方式强势回归。2020年左右,一些研究将注意力机制(Attention) 与Hopfield网络联系起来,提出了现代Hopfield网络。

其核心突破在于,将离散的 {+1, -1} 状态扩展为连续的、高维的向量,并定义了一个新的、容量大得多的能量函数。令人震惊的是,Transformer模型中的自注意力层,可以被证明是在执行一种现代Hopfield网络的更新步骤。

特性传统Hopfield网络现代Hopfield网络
状态表示离散二值 (±1)连续高维向量
能量函数二次型基于指数函数的更复杂形式
存储容量~0.15N指数级容量 (可存储大量模式)
更新规则符号函数Softmax函数 (与注意力机制一致)
主要应用小规模模式联想、概念验证理解Transformer、大规模联想记忆

这个联系非常深刻,它意味着我们正在使用的、支撑了GPT等大模型的Transformer,其核心运算之一可以追溯到40年前这个简单的联想记忆模型。这为我们理解深度学习的“黑箱”打开了一扇窗。

4.3 实际应用中的考量与技巧

虽然我们实现的是最基础的版本,但在实际应用中,可能会考虑以下改进:

  • 连续值/灰度图像:可以通过阈值处理将灰度图转换为二值图,或者使用改进的模型(如连续Hopfield网络)。
  • 偏置项:在我们的公式中省略了神经元的阈值(偏置)θ_i。加入偏置可以增加模型的灵活性,对应能量函数中的线性项。
  • 异步更新的变体:除了完全随机选择神经元,也可以按固定顺序更新,但随机顺序通常能带来更好的收敛性。
  • 收敛判断:我们的代码以“状态无变化”作为收敛标准。也可以设置一个能量变化的最小阈值 delta_E,当能量变化小于该阈值时停止。
  • 处理伪状态:网络可能收敛到非存储模式的稳定状态。一种缓解方法是使用模拟退火随机Hopfield网络(即玻尔兹曼机),在更新中引入随机性,帮助跳出局部最小值。

Hopfield网络是一个完美的教学工具,它用最精简的架构,展示了神经网络中分布式存储动力系统收敛能量最小化这些核心概念。亲手实现它,不仅能加深你对这些概念的理解,更能让你感受到,即使是最简单的规则,也能涌现出“智能”般的行为——从混乱中重建秩序。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐