Hopfield 网络实战:用Python从零搭建一个简单的联想记忆模型(附代码)
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网络有两个关键约束:- 无自连接:
w_ii = 0,神经元不连接自己。 - 对称性:
w_ij = w_ji,连接是对称的。这保证了网络动态演化的稳定性。
- 无自连接:
注意:权重矩阵
W就是网络的“长期记忆”。一旦通过训练确定下来,在回忆阶段就不会再改变。
1.2 “回忆”的过程:能量最小化
网络如何从模糊输入中找到正确记忆?这依赖于一个叫做能量函数的概念。对于给定的神经元状态向量 s,能量 E 定义为:
E = -0.5 * sum_i sum_j (w_ij * s_i * s_j)
这个公式的直观理解是:如果相连的神经元状态(s_i 和 s_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网络是一个完美的教学工具,它用最精简的架构,展示了神经网络中分布式存储、动力系统收敛和能量最小化这些核心概念。亲手实现它,不仅能加深你对这些概念的理解,更能让你感受到,即使是最简单的规则,也能涌现出“智能”般的行为——从混乱中重建秩序。
更多推荐


所有评论(0)