引言
感知机(Perceptron)是神经网络领域的奠基算法,由Frank Rosenblatt于1957年提出。作为最简单的线性分类模型,感知机奠定了现代深度学习的基础。本文将用Java从零实现感知机算法,深入讲解其数学原理、梯度下降权重更新机制,并通过完整的代码演示其在二分类问题中的应用。
算法原理
感知机模型
感知机是一个二分类线性模型,其输入为实例的特征向量,输出为实例的类别(+1或-1)。感知机模型可以表示为:
$$f(x) = \text{sign}(w \cdot x + b)$$
其中:
– $w$ 为权重向量
– $b$ 为偏置项
– $\text{sign}$ 为符号函数
损失函数
感知机使用误分类点到超平面的总距离作为损失函数:
$$L(w, b) = -\sum_{x_i \in M} y_i(w \cdot x_i + b)$$
其中 $M$ 为误分类点的集合。
梯度下降更新
对损失函数求梯度:
– $\nabla_w L = -\sum_{x_i \in M} y_i x_i$
– $\nabla_b L = -\sum_{x_i \in M} y_i$
采用随机梯度下降,每次选取一个误分类点 $(x_i, y_i)$ 进行更新:
– $w = w + \eta y_i x_i$
– $b = b + \eta y_i$
其中 $\eta$ 为学习率($0 < \eta \leq 1$)。
Java实现
项目结构
src/
├── Perceptron.java // 感知机核心类
├── DataPoint.java // 数据点类
├── PerceptronDemo.java // 演示入口
└── DatasetGenerator.java // 数据集生成器
数据点类
/**
* 数据点类,表示一个带有标签的特征向量
*/
public class DataPoint {
private final double[] features;
private final int label; // +1 或 -1
public DataPoint(double[] features, int label) {
this.features = features;
this.label = label;
}
public double[] getFeatures() {
return features;
}
public int getLabel() {
return label;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("(");
for (int i = 0; i < features.length; i++) {
sb.append(String.format("%.2f", features[i]));
if (i < features.length - 1) sb.append(", ");
}
sb.append(") -> ").append(label);
return sb.toString();
}
}
感知机核心类
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 感知机算法实现
* 支持多维度特征的二分类问题
*/
public class Perceptron {
private double[] weights; // 权重向量
private double bias; // 偏置项
private double learningRate; // 学习率
private int maxIterations; // 最大迭代次数
private int featureDim; // 特征维度
private List<Integer> misclassifiedHistory; // 误分类点数量历史
public Perceptron(int featureDim, double learningRate, int maxIterations) {
this.featureDim = featureDim;
this.learningRate = learningRate;
this.maxIterations = maxIterations;
this.weights = new double[featureDim];
this.bias = 0.0;
this.misclassifiedHistory = new ArrayList<>();
}
/**
* 使用随机梯度下降训练感知机
* @param dataPoints 训练数据集
*/
public void train(List<DataPoint> dataPoints) {
Random random = new Random(42); // 固定种子保证可复现
int iteration = 0;
System.out.println("=== 感知机训练开始 ===");
System.out.println("学习率: " + learningRate);
System.out.println("特征维度: " + featureDim);
System.out.println("训练样本数: " + dataPoints.size());
while (iteration < maxIterations) {
int misclassifiedCount = 0;
List<DataPoint> misclassifiedPoints = new ArrayList<>();
// 找出所有误分类点
for (DataPoint point : dataPoints) {
if (!isCorrectlyClassified(point)) {
misclassifiedPoints.add(point);
misclassifiedCount++;
}
}
misclassifiedHistory.add(misclassifiedCount);
// 如果没有误分类点,训练完成
if (misclassifiedCount == 0) {
System.out.println("训练完成!共迭代 " + (iteration + 1) + " 次");
break;
}
// 随机选择一个误分类点进行更新
DataPoint point = misclassifiedPoints.get(random.nextInt(misclassifiedPoints.size()));
updateWeights(point);
// 每100次迭代输出一次状态
if ((iteration + 1) % 100 == 0 || iteration < 10) {
System.out.printf("迭代 %d: 误分类点数量 = %d%n", iteration + 1, misclassifiedCount);
}
iteration++;
}
if (iteration >= maxIterations) {
System.out.println("达到最大迭代次数,训练终止");
}
}
/**
* 判断数据点是否被正确分类
*/
private boolean isCorrectlyClassified(DataPoint point) {
double[] features = point.getFeatures();
double sum = bias;
for (int i = 0; i < featureDim; i++) {
sum += weights[i] * features[i];
}
int predicted = sum >= 0 ? 1 : -1;
return predicted == point.getLabel();
}
/**
* 更新权重和偏置
* w = w + η * y * x
* b = b + η * y
*/
private void updateWeights(DataPoint point) {
double[] features = point.getFeatures();
int label = point.getLabel();
for (int i = 0; i < featureDim; i++) {
weights[i] += learningRate * label * features[i];
}
bias += learningRate * label;
}
/**
* 预测单个数据点的类别
*/
public int predict(DataPoint point) {
double[] features = point.getFeatures();
double sum = bias;
for (int i = 0; i < featureDim; i++) {
sum += weights[i] * features[i];
}
return sum >= 0 ? 1 : -1;
}
/**
* 计算决策边界的值
* 对于二维数据,返回 y = -(w0*x + b) / w1
*/
public double decisionBoundary(double x) {
if (featureDim == 2 && Math.abs(weights[1]) > 1e-10) {
return -(weights[0] * x + bias) / weights[1];
}
return 0;
}
/**
* 在测试集上评估模型
*/
public double evaluate(List<DataPoint> testData) {
int correct = 0;
for (DataPoint point : testData) {
if (predict(point) == point.getLabel()) {
correct++;
}
}
return (double) correct / testData.size();
}
public double[] getWeights() {
return weights.clone();
}
public double getBias() {
return bias;
}
public List<Integer> getMisclassifiedHistory() {
return misclassifiedHistory;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Perceptron{");
sb.append("weights=[");
for (int i = 0; i < weights.length; i++) {
sb.append(String.format("%.4f", weights[i]));
if (i < weights.length - 1) sb.append(", ");
}
sb.append("], bias=").append(String.format("%.4f", bias));
sb.append(", learningRate=").append(learningRate).append("}");
return sb.toString();
}
}
数据集生成器
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 生成线性可分的二分类数据集
*/
public class DatasetGenerator {
private Random random;
public DatasetGenerator(long seed) {
this.random = new Random(seed);
}
/**
* 生成二维线性可分数据集
* @param n 样本数量
* @param trueWeights 真实的权重向量(用于生成标签)
* @param trueBias 真实的偏置项
* @param noise 噪声水平
*/
public List<DataPoint> generateLinearDataset(int n, double[] trueWeights, double trueBias, double noise) {
List<DataPoint> dataset = new ArrayList<>();
int dim = trueWeights.length;
for (int i = 0; i < n; i++) {
double[] features = new double[dim];
for (int j = 0; j < dim; j++) {
features[j] = random.nextDouble() * 10 - 5; // [-5, 5]
}
// 计算真实标签
double sum = trueBias;
for (int j = 0; j < dim; j++) {
sum += trueWeights[j] * features[j];
}
// 添加噪声
sum += random.nextGaussian() * noise;
int label = sum >= 0 ? 1 : -1;
dataset.add(new DataPoint(features, label));
}
return dataset;
}
/**
* 生成AND逻辑门数据集
*/
public List<DataPoint> generateANDDataset() {
List<DataPoint> dataset = new ArrayList<>();
dataset.add(new DataPoint(new double[]{0, 0}, -1));
dataset.add(new DataPoint(new double[]{0, 1}, -1));
dataset.add(new DataPoint(new double[]{1, 0}, -1));
dataset.add(new DataPoint(new double[]{1, 1}, 1));
return dataset;
}
/**
* 生成OR逻辑门数据集
*/
public List<DataPoint> generateORDataset() {
List<DataPoint> dataset = new ArrayList<>();
dataset.add(new DataPoint(new double[]{0, 0}, -1));
dataset.add(new DataPoint(new double[]{0, 1}, 1));
dataset.add(new DataPoint(new double[]{1, 0}, 1));
dataset.add(new DataPoint(new double[]{1, 1}, 1));
return dataset;
}
/**
* 划分训练集和测试集
*/
public static void splitDataset(List<DataPoint> dataset, double trainRatio,
List<DataPoint> trainSet, List<DataPoint> testSet) {
int trainSize = (int) (dataset.size() * trainRatio);
for (int i = 0; i < dataset.size(); i++) {
if (i < trainSize) {
trainSet.add(dataset.get(i));
} else {
testSet.add(dataset.get(i));
}
}
}
}
演示入口
import java.util.ArrayList;
import java.util.List;
/**
* 感知机算法演示
*/
public class PerceptronDemo {
public static void main(String[] args) {
// 演示1:逻辑门分类
demoLogicGate();
System.out.println("\n" + "=".repeat(60) + "\n");
// 演示2:二维线性可分数据分类
demoLinearClassification();
}
/**
* 演示1:AND逻辑门分类
*/
private static void demoLogicGate() {
System.out.println("=== 演示1:AND逻辑门分类 ===\n");
DatasetGenerator generator = new DatasetGenerator(42);
List<DataPoint> andDataset = generator.generateANDDataset();
System.out.println("AND逻辑门数据集:");
for (DataPoint point : andDataset) {
System.out.println(" " + point);
}
Perceptron perceptron = new Perceptron(2, 0.1, 1000);
perceptron.train(andDataset);
System.out.println("\n训练结果:" + perceptron);
System.out.println("\n预测结果:");
for (DataPoint point : andDataset) {
int predicted = perceptron.predict(point);
System.out.printf(" %s -> 预测: %d, 实际: %d%n",
point, predicted, point.getLabel());
}
double accuracy = perceptron.evaluate(andDataset);
System.out.printf("\n准确率: %.1f%%%n", accuracy * 100);
}
/**
* 演示2:二维线性可分数据分类
*/
private static void demoLinearClassification() {
System.out.println("=== 演示2:二维线性可分数据分类 ===\n");
// 真实的决策边界: 2x + 3y - 5 = 0
double[] trueWeights = {2.0, 3.0};
double trueBias = -5.0;
DatasetGenerator generator = new DatasetGenerator(123);
List<DataPoint> fullDataset = generator.generateLinearDataset(
200, trueWeights, trueBias, 0.5);
List<DataPoint> trainSet = new ArrayList<>();
List<DataPoint> testSet = new ArrayList<>();
DatasetGenerator.splitDataset(fullDataset, 0.8, trainSet, testSet);
System.out.println("真实决策边界: 2x + 3y - 5 = 0");
System.out.println("训练集大小: " + trainSet.size());
System.out.println("测试集大小: " + testSet.size());
Perceptron perceptron = new Perceptron(2, 0.01, 5000);
perceptron.train(trainSet);
System.out.println("\n训练结果:" + perceptron);
double trainAccuracy = perceptron.evaluate(trainSet);
double testAccuracy = perceptron.evaluate(testSet);
System.out.printf("\n训练集准确率: %.1f%%%n", trainAccuracy * 100);
System.out.printf("测试集准确率: %.1f%%%n", testAccuracy * 100);
// 打印决策边界对比
System.out.println("\n决策边界对比(x=-2时):");
System.out.printf(" 真实边界 y = %.4f%n", (-trueWeights[0] * (-2) - trueBias) / trueWeights[1]);
System.out.printf(" 学习边界 y = %.4f%n", perceptron.decisionBoundary(-2));
}
}
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | $O(T \cdot n \cdot d)$ | $T$为迭代次数,$n$为样本数,$d$为特征维度 |
| 空间复杂度 | $O(d)$ | 仅需存储权重向量和偏置项 |
| 收敛性 | $O(1/\gamma^2)$ | 当数据线性可分时,Novikoff定理保证收敛 |
其中 $\gamma$ 为数据到超平面的最小间隔。
运行结果
=== 演示1:AND逻辑门分类 ===
AND逻辑门数据集:
(0.00, 0.00) -> -1
(0.00, 1.00) -> -1
(1.00, 0.00) -> -1
(1.00, 1.00) -> 1
=== 感知机训练开始 ===
学习率: 0.1
特征维度: 2
训练样本数: 4
迭代 1: 误分类点数量 = 1
迭代 2: 误分类点数量 = 1
迭代 3: 误分类点数量 = 1
训练完成!共迭代 4 次
训练结果:Perceptron{weights=[0.2000, 0.2000], bias=-0.3000, learningRate=0.1}
预测结果:
(0.00, 0.00) -> -1, 实际: -1 -> 预测: -1, 实际: -1
(0.00, 1.00) -> -1, 实际: -1 -> 预测: -1, 实际: -1
(1.00, 0.00) -> -1, 实际: -1 -> 预测: -1, 实际: -1
(1.00, 1.00) -> 1, 实际: 1 -> 预测: 1, 实际: 1
准确率: 100.0%
算法扩展
对偶形式
感知机还有对偶形式,通过Gram矩阵计算内积:
$$f(x) = \text{sign}\left(\sum_{j=1}^{N} \alpha_j y_j (x_j \cdot x) + b\right)$$
对偶形式适用于高维特征空间,可以利用核函数扩展到非线性分类。
多分类扩展
感知机可以扩展为多分类问题:
– 一对多(One-vs-Rest):为每个类别训练一个感知机
– 一对一(One-vs-One):为每对类别训练一个感知机
总结
感知机算法以其简洁优雅的设计,成为理解神经网络和机器学习的最佳入门算法。本文通过完整的Java实现,展示了随机梯度下降的核心机制——通过不断修正误分类点,逐步逼近最优决策边界。掌握感知机后,读者可以顺利过渡到更复杂的多层神经网络和深度学习方法。