【AI】感知机与多层感知机

Sunday, August 20, 2023
本文共1589字
4分钟阅读时长
posts , AI

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/ai%E6%84%9F%E7%9F%A5%E6%9C%BA%E4%B8%8E%E5%A4%9A%E5%B1%82%E6%84%9F%E7%9F%A5%E6%9C%BA/。商业转载请联系作者获得授权,非商业转载请注明出处!

Knowledge speaks, but wisdom listens. — Jimi Hendrix

感知机

感知机是一个简单的二分类线性分类器,是神经网络和深度学习的基石。它基于一个线性预测函数来进行预测,根据这个预测值再经过一个阈值函数来做二分类决策。

感知机模型的基本形式是:

$$ f(x) = \text{sign}(w \cdot x + b)

$$

其中:

  • $x$ 是输入向量。
  • $w$ 是权重向量。
  • $b$ 是偏置。
  • $w \cdot x$ 是 $w$ 和 $x$ 的点积。
  • $\text{sign}$ 是符号函数,如果它的参数为正则返回+1,如果参数为负则返回-1。

感知机的学习策略是通过迭代的方式,不断调整权重 $w$ 和偏置 $b$,以减少预测值与真实类标签之间的差异。

文内图片

文内图片

感知机难以处理XOR问题

XOR问题是什么

XOR问题可以看做是单位正方形的四个角,响应的输入模式为(0,0),(0,1),(1,1),(1,0)第一个和第三个模式属于类0,即输入模式(0,0)和(1,1)是单位正方形的两个相对的角,但它们产生相同的结果是0。另一方面,输入模式(0,1)和(1,0)是单位正方形另一对相对的角,但是它们属于类1。

文内图片

感知机的一个重要限制是它只能分类线性可分的数据集。也就是说,如果数据集中存在两个类,它们可以通过一个直线、平面或超平面完全分开,那么感知机可以找到这个分类边界。但如果数据是线性不可分的,那么感知机将不能找到一个完美的分类边界。

感知机的代码实现

感知机比较简单,这里给出一个numpy的实现。注意,训练数据和测试数据一定要是线性可分的

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

def generate_data(num_samples):
    # 定义直线 ax + by + c = 0
    a, b = np.random.uniform(-10, 10, 2)  # 随机选择a和b
    c = np.random.uniform(-10, 10)  # 随机选择c

    # 生成随机点
    X = np.random.uniform(-10, 10, (num_samples, 2))

    # 根据点到直线的距离给予标签
    # d = (a*x + b*y + c) / sqrt(a^2 + b^2)
    distances = (a * X[:, 0] + b * X[:, 1] + c) / (np.sqrt(a**2 + b**2))
    y = np.sign(distances)

    return X, y

# 生成训练数据和测试数据
X, y = generate_data(200)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

w, b = np.zeros_like(X_train[0]), 0
epoch = 100
lr = 0.1
flag = True
while flag:
    flag = False
    for i in range(len(X_train)):
        pred = w @ X_train[i].T + b
        if y_train[i] * pred <= 0:
            w_grad = lr * y_train[i] * X_train[i]
            b_grad = lr * y_train[i]
            w += w_grad.T
            b += b_grad
            flag = True
def plot_line_by_slope_intercept(slope, intercept, xmin=-10, xmax=10, color='g', label=None):
    x_vals = np.linspace(xmin, xmax, 400)
    y_vals = slope * x_vals + intercept
    plt.plot(x_vals, y_vals, color=color, label=label)
plt.figure(figsize=(6, 6))
plt.scatter(X_test[y_test == 1][:, 0], X_test[y_test == 1][:, 1], c='r', label='positive', alpha=0.8)
plt.scatter(X_test[y_test == -1][:, 0], X_test[y_test == -1][:, 1], c='b', label='negative', alpha=0.8)
plot_line_by_slope_intercept(-w[0]/w[1], -b/w[1], color='g', label='Hyperplane')
# 使用 quiver 函数绘制法向量
plt.quiver(0, 0, w[0], w[1], angles='xy', scale_units='xy', scale=1, color='black', label='Normal Vector')
plt.legend()
ax = plt.gca()
ax.set_aspect('equal')
plt.show()

文内图片

多层感知机

多层感知机 (MLP, Multi-Layer Perceptron) 是一种前馈式的人工神经网络,包含至少三层节点:输入层、至少一个隐藏层、和一个输出层。每个节点(除输入层外)都是一个神经元,或称为“感知机”,使用一个非线性激活函数。MLP 是一种全连接网络,也就是说,每一层的所有节点都与下一层的所有节点相连。

文内图片

MLP 的数学表达可以由以下几个部分构成:

  1. 线性加权和:给定输入 $x$,权重 $W$ 和偏置 $b$,线性加权和表示为: $z = Wx + b$

  2. 非线性激活函数:为了引入非线性性质,我们应用一个非线性激活函数 $f$ 到上述的加权和。常见的激活函数包括 sigmoid、ReLU (Rectified Linear Unit)、tanh 等。 $a = f(z)$ 其中,$a$ 是激活后的输出。

为了表达一个具有一个隐藏层的 MLP:

  1. 第一层(输入到隐藏): $z^{[1]} = W^{[1]}x + b^{[1]}$ $a^{[1]} = f(z^{[1]})$

  2. 第二层(隐藏到输出): $z^{[2]} = W^{[2]}a^{[1]} + b^{[2]}$ $a^{[2]} = f(z^{[2]})$

其中:

  • $x$ 是输入向量。
  • $W^{[1]}$ 和 $b^{[1]}$ 是第一层的权重和偏置。
  • $W^{[2]}$ 和 $b^{[2]}$ 是第二层的权重和偏置。
  • $f$ 是激活函数。

多层感知机一定要一个非线性的激活函数,不然多个线性函数叠加在一起还是一个线性函数

文内图片

代码实现

多层感知机的numpy实现涉及一些梯度计算,因此这里给出pytorch的实现:

import tqdm
import torch
from torch import nn
import numpy as np 
from torch.utils.data import Dataset, DataLoader
from sklearn.metrics import accuracy_score

# 数据初始化
train_shape = (10000, 64)
X_train = torch.randn(train_shape[0], train_shape[1], dtype=torch.float)
y_train = torch.tensor(np.random.choice([-1, 1], (train_shape[0], 1)), dtype=torch.float)
test_shape = (1000, 64)
X_test = torch.randn(test_shape[0], test_shape[1], dtype=torch.float)
y_test = torch.tensor(np.random.choice([-1, 1], (test_shape[0], 1)), dtype=torch.float)

class MyDataset(Dataset):
    def __init__(self, data, targets):
        self.data = data
        self.targets = targets
    
    def __len__(self):
        return len(self.data)
    
    def __getitem__(self, index):
        x = self.data[index]
        y = self.targets[index]
        return x, y

class MLP(nn.Module):
    def __init__(self, in_size, out_size):
        super().__init__()
        self.hidden1 = nn.Linear(in_size, in_size//2)
        self.hidden2 = nn.Linear(in_size//2, in_size//4)
        self.relu = nn.ReLU()
        self.output = nn.Linear(in_size//4, out_size)
        # self.relu2 = nn.ReLU()
    def forward(self, X):
        X = self.hidden1(X)
        X = self.relu(X)
        X = self.hidden2(X)
        X = self.relu(X)
        X = self.output(X)
        return X

mlp_net = MLP(64, 1)
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(mlp_net.parameters(), lr=0.01)
# 创建数据集实例
train_dataset = MyDataset(X_train, y_train)
# 使用DataLoader进行分批
batch_size = 64
dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
epoch = 100

for i in tqdm.tqdm(range(epoch)):
    for batch_idx, (data_batch, target_batch) in enumerate(dataloader):
        # 前向传递
        y = mlp_net(data_batch)
        target_batch = (target_batch > 0).to(torch.float)
        loss = criterion(y, target_batch)
        # 后向传递
        optimizer.zero_grad() # 清除之前的梯度
        loss.backward() # 计算loss的梯度
        optimizer.step() # 反向传递
    # print(f"epoch {i}: \n\t{mlp_net.state_dict()}")
test_dataset = MyDataset(X_test, y_test)
dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
mlp_net.eval()
y_true = []
y_pred = []
with torch.no_grad():
    for batch_idx, (data_batch, target_batch) in enumerate(dataloader):
        # 前向传递
        y = mlp_net(data_batch)
        y = [-1 if yi == 0 else 1 for yi in y]
        y_pred.extend(y)
        y_true.extend(target_batch.tolist())
ac = accuracy_score(y_true, y_pred)
print(f"{ac=}")