机器学习Python教程
Section outline
-
分类器 (Classifier)
分类器是指一个能将未标记实例映射到类别的程序或函数。
混淆矩阵 (Confusion Matrix)
混淆矩阵,也称为列联表或误差矩阵,用于可视化分类器的性能。
矩阵的列表示预测类别的实例,而行表示实际类别的实例。(注意:这也可以反过来。)
在二元分类的情况下,该表有 2 行 2 列。
示例:
这意味着分类器正确预测了 42 个男性实例,错误地将 8 个男性实例预测为女性。它正确预测了 32 个女性实例。有 18 个实例被错误地预测为男性而非女性。
准确率 (Accuracy / Error Rate)
准确率是一个统计度量,定义为分类器做出的正确预测数除以分类器做出的预测总数。
我们上一个例子中的分类器正确预测了 42 个男性实例和 32 个女性实例。因此,准确率可以计算为:
准确率 = (42 + 32) / (42 + 8 + 18 + 32) = 0.72
让我们假设我们有一个分类器,它总是预测“女性”。在这种情况下,我们的准确率为 50%。
我们将演示所谓的准确率悖论。
一个垃圾邮件识别分类器由以下混淆矩阵描述:
该分类器的准确率为 (4 + 91) / 100,即 95%。
以下分类器仅预测“非垃圾邮件”,并且具有相同的准确率。
这个分类器的准确率是 95%,尽管它完全无法识别任何垃圾邮件。
精确率 (Precision) 和 召回率 (Recall)
-
准确率 (Accuracy):
-
精确率 (Precision):
-
召回率 (Recall):
监督学习 (Supervised Learning)
机器学习程序被赋予输入数据和相应的标签。这意味着学习数据必须事先由人工标记。
无监督学习 (Unsupervised Learning)
没有向学习算法提供标签。算法必须自行找出输入数据的聚类。
强化学习 (Reinforcement Learning)
计算机程序与其环境动态交互。这意味着程序会收到正向和/或负向反馈以提高其性能。
CLASSIFIERA program or a function which maps from unlabeled instances to classes is called a classifier.CONFUSION MATRIXA confusion matrix, also called a contingeny table or error matrix, is used to visualize the performance of aclassifier.The columns of the matrix represent the instances of the predicted classes and the rows represent the instancesof the actual class. (Note: It can be the other way around as well.)In the case of binary classification the table has 2 rows and 2 columns.Example:3ConfusionMatrixPredictedmaleclassesfemalecl a sA c male428tsueaslfemale1832This means that the classifier correctly predicted a male person in 42 cases and it wrongly predicted 8 maleinstances as female. It correctly predicted 32 instances as female. 18 cases had been wrongly predicted as maleinstead of female.ACCURACY (ERROR RATE)Accuracy is a statistical measure which is defined as the quotient of correct predictions made by a classifierdivided by the sum of predictions made by the classifier.The classifier in our previous example predicted correctly predicted 42 male instances and 32 female instance.Therefore, the accuracy can be calculated by:accuracy = (42 + 32) / (42 + 8 + 18 + 32)which is 0.72Let's assume we have a classifier, which always predicts "female". We have an accuracy of 50 % in this case.ConfusionMatrixPredictedmaleclassesfemalecl a sA c male050stueaslfemale050We will demonstrate the so-called accuracy paradox.A spam recogition classifier is described by the following confusion matrix:4ConfusionMatrixPredictedspamclasseshamcl a sA c spam41tsueaslham491The accuracy of this classifier is (4 + 91) / 100, i.e. 95 %.The following classifier predicts solely "ham" and has the same accuracy.ConfusionMatrixPredictedspamclasseshamcl a sA c spam05tsueaslham095The accuracy of this classifier is 95%, even though it is not capable of recognizing any spam at all.PRECISION AND RECALLConfusionMatrixPredictednegativeclassespositivecl a sA c negativeTNFPtsueaslpositiveFNTPAccuracy: (TN + TP) / (TN + TP + FN + FP)Precision: TP / (TP + FP)5Recall: TP / (TP + FN)SUPERVISED LEARNINGThe machine learning program is both given the input data and the corresponding labelling. This means thatthe learn data has to be labelled by a human being beforehand.UNSUPERVISED LEARNINGNo labels are provided to the learning algorithm. The algorithm has to figure out the a clustering of the inputdata.REINFORCEMENT LEARNINGA computer program dynamically interacts with its environment. This means that the program receivespositive and/or negative feedback to improve it performance. -
-
机器学习简介:数据、经验与评估
机器学习的核心在于让模型适应数据。因此,首先我们需要了解数据如何被表示,以便计算机能够理解。
在本章开头,我们引用了汤姆·米切尔 (Tom Mitchell) 对机器学习的定义:“一个设计良好的学习问题:如果一个计算机程序在任务 T 上的表现,由性能度量 P 来衡量,通过经验 E 得到提升,那么就称该程序从经验 E 中学习。”数据是机器学习的“原材料”,机器学习正是从数据中学习。在米切尔的定义中,“数据”隐藏在“经验 E”和“性能度量 P”这两个术语背后。如前所述,我们需要带标签的数据来训练和测试我们的算法。
然而,在开始训练分类器之前,我们强烈建议您熟悉您的数据。Numpy 提供了理想的数据结构来表示您的数据,而 Matplotlib 则为数据可视化提供了强大的功能。
接下来,我们将使用 sklearn 模块中的数据来演示如何完成这些操作。
Iris 数据集:机器学习界的“Hello World”
您看过的第一个程序是什么?我敢打赌,很可能是一个用某种编程语言输出“Hello World”的程序。我大概率是对的。几乎所有编程入门书籍或教程都以这样的程序开始。这个传统可以追溯到 1968 年布莱恩·柯尼汉 (Brian Kernighan) 和丹尼斯·里奇 (Dennis Ritchie) 合著的《C 语言程序设计》一书!
同样,您在机器学习入门教程中看到的第一个数据集极有可能是“Iris 数据集”。Iris 数据集包含了来自 3 种不同鸢尾花(Iris)的 150 个样本的测量数据:
-
Setosa(山鸢尾)
-
Versicolor(变色鸢尾)
-
Virginica(维吉尼亚鸢尾)
Iris 数据集因其简单性而经常被使用。这个数据集包含在 scikit-learn 中,但在深入研究 Iris 数据集之前,我们先来看看 scikit-learn 中可用的其他数据集。
Machine learning is about adapting
models to data. For this reason we begin
by showing how data can be represented
in order to be understood by the computer.
At the beginning of this chapter we quoted
Tom Mitchell's definition of machine
learning: "Well posed Learning Problem:
A computer program is said to learn from
experience E with respect to some task T
and some performance measure P, if its
performance on T, as measured by P,
improves with experience E." Data is the
"raw material" for machine learning. It
learns from data. In Mitchell's definition,
"data" is hidden behind the terms
"experience E" and "performance measure
P". As mentioned earlier, we need labeled
data to learn and test our algorithm.
However, it is recommended that you
familiarize yourself with your data before
you begin training your classifier.
Numpy offers ideal data structures to
represent your data and Matplotlib offers great possibilities for visualizing your data.
In the following, we want to show how to do this using the data in the sklearn module.
IRIS DATASET, "HELLO WORLD" OF MACHINE LEARNING
What was the first program you saw? I bet it might have been a program giving out "Hello World" in some
programming language. Most likely I'm right. Almost every introductory book or tutorial on programming
starts with such a program. It's a tradition that goes back to the 1968 book "The C Programming Language" by
Brian Kernighan and Dennis Ritchie!
The likelihood that the first dataset you will see in an introductory tutorial on machine learning will be the
"Iris dataset" is similarly high. The Iris dataset contains the measurements of 150 iris flowers from 3 different
species:
••Iris-Setosa,
Iris-Versicolor,and
15
IrisIrisIris• Iris-Virginica.
Setosa
Versicolor
Virginica
16
The iris dataset is often used for its simplicity. This dataset is contained in scikit-learn, but before we have a
deeper look into the Iris dataset we will look at the other datasets available in scikit-learn. -
-
例如,scikit-learn 提供了关于这些鸢尾花物种的非常直接的数据集。该数据集包含以下内容:
-
Iris 数据集的特征(Features):
-
萼片长度(单位:厘米)
-
萼片宽度(单位:厘米)
-
花瓣长度(单位:厘米)
-
花瓣宽度(单位:厘米)
-
-
要预测的目标类别(Target classes):
-
鸢尾花-Setosa
-
鸢尾花-Versicolor
-
鸢尾花-Virginica
-
scikit-learn 内嵌了一份 Iris CSV 文件,并提供了一个辅助函数来将其加载到 numpy 数组中:
Pythonfrom sklearn.datasets import load_iris iris = load_iris()
生成的数据集是一个 Bunch 对象:
Pythontype(iris)
输出:
sklearn.utils.Bunch
您可以使用
keys()
方法查看此数据类型可用的内容:Pythoniris.keys()
输出:
dict_keys(['data', 'target', 'target_names', 'DESCR', 'feature_names', 'filename'])
Bunch 对象类似于字典,但它还允许以属性方式访问键:
Pythonprint(iris["target_names"]) print(iris.target_names)
输出:
['setosa' 'versicolor' 'virginica'] ['setosa' 'versicolor' 'virginica']
每个样本花的特征存储在数据集的
data
属性中:Pythonn_samples, n_features = iris.data.shape print('样本数量:', n_samples) print('特征数量:', n_features) # 第一个样本(第一朵花)的萼片长度、萼片宽度、花瓣长度和花瓣宽度 print(iris.data[0])
输出:
样本数量: 150 特征数量: 4 [5.1 3.5 1.4 0.2]
每朵花的特征都存储在数据集的
data
属性中。让我们看一些样本:Python# 索引为 12, 26, 89 和 114 的花 iris.data[[12, 26, 89, 114]]
输出:
array([[4.8, 3. , 1.4, 0.1], [5. , 3.4, 1.6, 0.4], [5.5, 2.5, 4. , 1.3], [5.8, 2.8, 5.1, 2.4]])
关于每个样本类别的信息,即标签,存储在数据集的
target
属性中:Pythonprint(iris.data.shape) print(iris.target.shape)
输出:
(150, 4) (150,)
Pythonprint(iris.target)
输出:
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 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 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]
通过使用 NumPy 的
bincount
函数,我们可以看到该数据集中的类别分布均匀——每个物种有 50 朵花:Pythonimport numpy as np np.bincount(iris.target)
输出:
array([50, 50, 50])
-
类别 0: 鸢尾花-Setosa
-
类别 1: 鸢尾花-Versicolor
-
类别 2: 鸢尾花-Virginica
这些类别名称存储在最后一个属性,即
target_names
中:Pythonprint(iris.target_names)
输出:
['setosa' 'versicolor' 'virginica']
我们 Iris 数据集中每个样本类别的信息存储在数据集的
target
属性中:Pythonprint(iris.target)
输出:
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 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 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]
除了数据本身的形状,我们还可以检查标签(即
target.shape
)的形状:每个花样本是数据数组中的一行,列(特征)表示以厘米为单位的花的测量值。例如,我们可以用以下格式表示这个由150个样本和4个特征组成的虹膜数据集,一个二维数组或矩阵r150 × 4:
上标表示第i行,下标表示第j个特征。一般来说,我们有n行k列:
Pythonprint(iris.data.shape) print(iris.target.shape)
输出:
(150, 4) (150,)
NumPy 的
bincount
函数可以计算非负整数数组中每个值的出现次数。我们可以用它来检查数据集中类别的分布:Pythonimport numpy as np np.bincount(iris.target)
输出:
array([50, 50, 50])
我们可以看到这些类别是均匀分布的——每个物种有 50 朵花,即:
-
类别 0: 鸢尾花-Setosa
-
类别 1: 鸢尾花-Versicolor
-
类别 2: 鸢尾花-Virginica
这些类别名称存储在最后一个属性,即
target_names
中:Pythonprint(iris.target_names)
输出:
['setosa' 'versicolor' 'virginica']
For example, scikit-learn has a very straightforward set of data on these iris species. The data consist of the
following:
• Features in the Iris dataset:
1.
2.
3.
4.
sepal length in cm
sepal width in cm
petal length in cm
petal width in cm
• Target classes to predict:
1.2.3.IrisIrisIrisSetosa
Versicolour
Virginica
scikit-learnarrays:
embeds a copy of the iris CSV file along with a helper function to load it into numpy
18
from sklearn.datasets import load_iris
iris = load_iris()
The resulting dataset is a Bunch object:
type(iris)
Output:sklearn.utils.Bunch
You can see what's available for this data type by using the method keys() :
iris.keys()
Output:dict_keys(['data', 'target', 'target_names', 'DESCR', 'featur
e_names', 'filename'])
A Bunch object is similar to a dicitionary, but it additionally allows accessing the keys in an attribute style:
print(iris["target_names"])
print(iris.target_names)
['setosa' 'versicolor' 'virginica']
['setosa' 'versicolor' 'virginica']
The features of each sample flower are stored in the data attribute of the dataset:
n_samples, n_features = iris.data.shape
print('Number of samples:', n_samples)
print('Number of features:', n_features)
# the sepal length, sepal width, petal length and petal width of t
he first sample (first flower)
print(iris.data[0])
Number of samples: 150
Number of features: 4
[5.1 3.5 1.4 0.2]
The feautures of each flower are stored in the data attribute of the data set. Let's take a look at some of the
samples:
# Flowers with the indices 12, 26, 89, and 114
iris.data[[12, 26, 89, 114]]
19
Output:array([[4.8, 3. , 1.4, 0.1],
[5. , 3.4, 1.6, 0.4],
[5.5, 2.5, 4. , 1.3],
[5.8, 2.8, 5.1, 2.4]])
The information about the class of each sample, i.e. the labels, is stored in the "target" attribute of the data set:
print(iris.data.shape)
print(iris.target.shape)
(150, 4)
(150,)
print(iris.target)
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 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 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 2 2 2 2 2 2
2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2
2 2]
import numpy as np
np.bincount(iris.target)
Output:array([50, 50, 50])
Using NumPy's bincount function (above) we can see that the classes in this dataset are evenly distributed -
there are 50 flowers of each species, with
•••class 0: Iris Setosa
class 1: Iris Versicolor
class 2: Iris Virginica
These class names are stored in the last attribute, namely target_names :
print(iris.target_names)
['setosa' 'versicolor' 'virginica']
20
The information about the class of each sample of our Iris dataset is stored in the target attribute of the
dataset:
print(iris.target)
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 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 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 2 2 2 2 2 2
2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2
2 2]
Beside of the shape of the data, we can also check the shape of the labels, i.e. the target.shape :
Each flower sample is one row in the data array, and the columns (features) represent the flower measurements
in centimeters. For instance, we can represent this Iris dataset, consisting of 150 samples and 4 features, a
2-dimensional array or matrix R 150 × 4 in the following format:
X =
[
x x x 1
(1
1
150 (2)
(1)
)
xx x 2
(2
2
(1)
(2)
150 )
xx x 3
( 3
3
(1)
150 (2)
)
x x x 4
(4
4
150 (2)
(1)
)
]
.
The superscript denotes the ith row, and the subscript denotes the jth feature, respectively.
Generally, we have n rows and k columns:
X =
[
x x x 1
1
1
(2)
(1)
( n )
x x x2
2
2
(2)
( (1)
n )
x x x 3
3
3
(1)
(2)
( n )
...
...
...
x x x k
k
k
( (2)
(1)
n )
]
.
print(iris.data.shape)
21
print(iris.target.shape)
(150, 4)
(150,)
bincount of NumPy counts the number of occurrences of each value in an array of non-negative integers.
We can use this to check the distribution of the classes in the dataset:
import numpy as np
np.bincount(iris.target)
Output:array([50, 50, 50])
We can see that the classes are distributed uniformly - there are 50 flowers from each species, i.e.
•••class 0: Iris-Setosa
class 1: Iris-Versicolor
class 2: Iris-Virginica
These class names are stored in the last attribute, namely target_names :
print(iris.target_names)
['setosa' 'versicolor' 'virginica'] -
-
特征数据是四维的,但我们可以通过简单的直方图或散点图一次性可视化其中的一到两个维度。
Pythonfrom sklearn.datasets import load_iris iris = load_iris() # 打印 target 为 1 的前 5 个样本数据 print(iris.data[iris.target==1][:5]) # 打印 target 为 1 的前 5 个样本的第 0 个特征 print(iris.data[iris.target==1, 0][:5])
输出:
[[7. 3.2 4.7 1.4] [6.4 3.2 4.5 1.5] [6.9 3.1 4.9 1.5] [5.5 2.3 4. 1.3] [6.5 2.8 4.6 1.5]] [7. 6.4 6.9 5.5 6.5]
特征直方图
我们可以使用直方图来可视化单个特征的分布,并按类别进行区分。
Pythonimport matplotlib.pyplot as plt fig, ax = plt.subplots() x_index = 3 # 选择要可视化的特征索引 (例如:3 代表花瓣宽度) colors = ['blue', 'red', 'green'] # 遍历每个类别并绘制直方图 for label, color in zip(range(len(iris.target_names)), colors): ax.hist(iris.data[iris.target==label, x_index], label=iris.target_names[label], color=color) ax.set_xlabel(iris.feature_names[x_index]) # 设置 x 轴标签为特征名称 ax.legend(loc='upper right') # 显示图例 fig.show()
练习
请查看其他特征(即花瓣长度、萼片宽度和萼片长度)的直方图。
两个特征的散点图
散点图可以同时展示两个特征在同一张图中的关系:
Pythonimport matplotlib.pyplot as plt fig, ax = plt.subplots() x_index = 3 # x 轴特征索引 (例如:3 代表花瓣宽度) y_index = 0 # y 轴特征索引 (例如:0 代表萼片长度) colors = ['blue', 'red', 'green'] # 遍历每个类别并绘制散点图 for label, color in zip(range(len(iris.target_names)), colors): ax.scatter(iris.data[iris.target==label, x_index], iris.data[iris.target==label, y_index], label=iris.target_names[label], c=color) ax.set_xlabel(iris.feature_names[x_index]) # 设置 x 轴标签 ax.set_ylabel(iris.feature_names[y_index]) # 设置 y 轴标签 ax.legend(loc='upper left') # 显示图例 plt.show()
练习
在上面的脚本中改变
x_index
和y_index
,找到一个能够最大程度地区分这三个类别的两个参数组合。
泛化
我们现在将所有特征组合在一个综合图中进行展示:
Pythonimport matplotlib.pyplot as plt n = len(iris.feature_names) # 特征数量 fig, ax = plt.subplots(n, n, figsize=(16, 16)) # 创建 n x n 的子图网格 colors = ['blue', 'red', 'green'] # 遍历所有特征组合 for x in range(n): for y in range(n): xname = iris.feature_names[x] yname = iris.feature_names[y] # 遍历每个类别并绘制散点图 for color_ind in range(len(iris.target_names)): ax[x, y].scatter(iris.data[iris.target==color_ind, x], iris.data[iris.target==color_ind, y], label=iris.target_names[color_ind], c=colors[color_ind]) ax[x, y].set_xlabel(xname) # 设置 x 轴标签 ax[x, y].set_ylabel(yname) # 设置 y 轴标签 ax[x, y].legend(loc='upper left') # 显示图例 plt.show()
The feauture data is four dimensional, but we can visualize one or two of the dimensions at a time using a
simple histogram or scatter-plot.
from sklearn.datasets import load_iris
iris = load_iris()
print(iris.data[iris.target==1][:5])
print(iris.data[iris.target==1, 0][:5])
[[7. 3.2 4.7 1.4]
[6.4 3.2 4.5 1.5]
[6.9 3.1 4.9 1.5]
[5.5 2.3 4. 1.3]
[6.5 2.8 4.6 1.5]]
[7. 6.4 6.9 5.5 6.5]
HISTOGRAMS OF THE FEATURES
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
x_index = 3
colors = ['blue', 'red', 'green']
for label, color in zip(range(len(iris.target_names)), colors):
ax.hist(iris.data[iris.target==label, x_index],
label=iris.target_names[label],
color=color)
ax.set_xlabel(iris.feature_names[x_index])
ax.legend(loc='upper right')
fig.show()
23
EXERCISE
Look at the histograms of the other features, i.e. petal length, sepal widt and sepal length.
SCATTERPLOT WITH TWO FEATURES
The appearance diagram shows two features in one diagram:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
x_index = 3
y_index = 0
colors = ['blue', 'red', 'green']
for label, color in zip(range(len(iris.target_names)), colors):
ax.scatter(iris.data[iris.target==label, x_index],
iris.data[iris.target==label, y_index],
label=iris.target_names[label],
c=color)
ax.set_xlabel(iris.feature_names[x_index])
ax.set_ylabel(iris.feature_names[y_index])
ax.legend(loc='upper left')
plt.show()
24
EXERCISE
Change x_index and y_index in the above script
Change x_index and y_index in the above script and find a combination of two parameters which maximally
separate the three classes.
GENERALIZATION
We will now look at all feature combinations in one combined diagram:
import matplotlib.pyplot as plt
n = len(iris.feature_names)
fig, ax = plt.subplots(n, n, figsize=(16, 16))
colors = ['blue', 'red', 'green']
for x in range:
for y in range:
xname = iris.feature_names[x]
yname = iris.feature_names[y]
for color_ind in range(len(iris.target_names)):
ax[x, y].scatter(iris.data[iris.target==color_ind,
x],
iris.data[iris.target==color_ind, y],
label=iris.target_names[color_ind],
c=colors[color_ind])
25
ax[x, y].set_xlabel(xname)
ax[x, y].set_ylabel(yname)
ax[x, y].legend(loc='upper left')
plt.show() -
我们也可以不手动操作,而是使用 pandas 模块提供的散点图矩阵。
散点图矩阵可以显示数据集中所有特征之间的散点图,以及每个特征的分布直方图。
Pythonimport pandas as pd import matplotlib.pyplot as plt # 导入 matplotlib 以便显示图表 # 将 Iris 数据转换为 Pandas DataFrame iris_df = pd.DataFrame(iris.data, columns=iris.feature_names) # 生成散点图矩阵 pd.plotting.scatter_matrix(iris_df, c=iris.target, # 根据目标类别着色 figsize=(8, 8) # 设置图表大小 ) plt.show() # 显示图表
3D 可视化
为了更全面地理解数据,我们可以尝试进行三维可视化。
Pythonimport matplotlib.pyplot as plt from sklearn.datasets import load_iris from mpl_toolkits.mplot3d import Axes3D # 导入 3D 绘图工具 iris = load_iris() X = [] for iclass in range(3): X.append([[], [], []]) # 为每个类别初始化三个空列表,分别用于存储 x, y, z 坐标 # 遍历 Iris 数据集,根据类别将数据分配到 X 中 for i in range(len(iris.data)): if iris.target[i] == iclass: X[iclass][0].append(iris.data[i][0]) # 萼片长度作为 x 轴 X[iclass][1].append(iris.data[i][1]) # 萼片宽度作为 y 轴 X[iclass][2].append(sum(iris.data[i][2:])) # 花瓣长度和花瓣宽度之和作为 z 轴 colours = ("r", "g", "y") # 定义不同类别的颜色 (红、绿、黄) fig = plt.figure() ax = fig.add_subplot(111, projection='3d') # 创建一个 3D 子图 # 为每个类别绘制散点图 for iclass in range(3): ax.scatter(X[iclass][0], X[iclass][1], X[iclass][2], c=colours[iclass]) plt.show() # 显示 3D 散点图
Instead of doing it manually we can also use the scatterplot matrix provided by the pandas module.
Scatterplot matrices show scatter plots between all features in the data set, as well as histograms to show the
distribution of each feature.
import pandas as pd
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)
pd.plotting.scatter_matrix(iris_df,
c=iris.target,
figsize=(8, 8)
);
27
3-DIMENSIONAL VISUALIZATION
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from mpl_toolkits.mplot3d import Axes3D
iris = load_iris()
X = []
for iclass in range(3):
X.append([[], [], []])
for i in range(len(iris.data)):
if iris.target[i] == iclass:
X[iclass][0].append(iris.data[i][0])
X[iclass][1].append(iris.data[i][1])
X[iclass][2].append(sum(iris.data[i][2:]))
colours = ("r", "g", "y")
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
for iclass in range(3):
ax.scatter(X[iclass][0], X[iclass][1], X[iclass][2], c=colour
s[iclass])
plt.show() -
Scikit-learn 提供了大量的数据集,用于测试学习算法。它们主要分为三种类型:
-
打包数据 (Packaged Data):这些小型数据集与 scikit-learn 安装包一同提供,可以使用
sklearn.datasets.load_*
工具进行加载。 -
可下载数据 (Downloadable Data):这些大型数据集可供下载,scikit-learn 提供了简化下载过程的工具。这些工具可以在
sklearn.datasets.fetch_*
中找到。 -
生成数据 (Generated Data):有几种数据集是基于随机种子从模型中生成的。这些可以在
sklearn.datasets.make_*
中获取。
您可以使用 IPython 的 Tab 补全功能来探索可用的数据集加载器、抓取器和生成器。在从
sklearn
导入datasets
子模块后,输入:datasets.load_<TAB>
或
datasets.fetch_<TAB>
或
datasets.make_<TAB>
即可查看可用函数的列表。
数据和标签的结构
Scikit-learn 中的数据在大多数情况下都保存为二维的 Numpy 数组,其形状为
(n, m)
。许多算法也接受相同形状的scipy.sparse
矩阵。-
n
(n_samples):样本数量。每个样本都是一个需要处理(例如分类)的项。一个样本可以是一篇文档、一张图片、一段声音、一段视频、一个天文物体、数据库或 CSV 文件中的一行,或者任何您可以用一组固定的定量特征来描述的事物。 -
m
(n_features):特征数量,即可以定量描述每个项的独特属性的数量。特征通常是实数值,但在某些情况下也可以是布尔值或离散值。
Pythonfrom sklearn import datasets
请注意:这些数据集中的许多都相当大,可能需要很长时间才能下载!
Scikit-learn makes available a host of
datasets for testing learning algorithms.
They come in three flavors:
•Packaged Data: these small
datasets are packaged with
the scikit-learn installation,
and can be downloaded
using the tools in
••sklearn.datasets.load_*
Downloadable Data: these larger datasets are available for download, and scikit-learn includes
tools which streamline this process. These tools can be found in
sklearn.datasets.fetch_*
Generated Data: there are several datasets which are generated from models based on a random
seed. These are available in the sklearn.datasets.make_*
You can explore the available dataset loaders, fetchers, and generators using IPython's tab-completion
functionality. After importing the datasets submodule from sklearn , type
datasets.load_<TAB>
or
datasets.fetch_<TAB>
or
datasets.make_<TAB>
to see a list of available functions.
STRUCTURE OF DATA AND LABELS
Data in scikit-learn is in most cases saved as two-dimensional Numpy arrays with the shapealgorithms also accept scipy.sparse matrices of the same shape.
(n, m) . Many
29
••n: (n_samples) The number of samples: each sample is an item to process (e.g. classify). A
sample can be a document, a picture, a sound, a video, an astronomical object, a row in database
or CSV file, or whatever you can describe with a fixed set of quantitative traits.
m: (n_features) The number of features or distinct traits that can be used to describe each item in
a quantitative manner. Features are generally real-valued, but may be Boolean or discrete-valued
in some cases.
from sklearn import datasets
Be warned: many of these datasets are quite large, and can take a long time to download! -
-
我们将更深入地研究这些数据集中的一个。我们来看一下数字数据集 (digits data set)。我们先加载它:
Pythonfrom sklearn.datasets import load_digits digits = load_digits()
同样,我们可以通过查看 "keys" 来获取可用属性的概览:
Pythondigits.keys()
输出:
dict_keys(['data', 'target', 'target_names', 'images', 'DESCR'])
我们来看看项目和特征的数量:
Pythonn_samples, n_features = digits.data.shape print((n_samples, n_features))
输出:
(1797, 64)
Pythonprint(digits.data[0]) print(digits.target)
输出:
[ 0. 0. 5. 13. 9. 1. 0. 0. 0. 0. 13. 15. 10. 15. 5. 0. 0. 3. 15. 2. 0. 11. 8. 0. 0. 4. 12. 0. 0. 8. 8. 0. 0. 5. 16. 8. 0. 0. 9. 8. 0. 0. 4. 11. 0. 1. 12. 7. 0. 0. 2. 14. 5. 0. 0. 0. 0. 6. 13. 10. 0. 0. 0.] [0 1 2 ... 8 9 8]
数据也可以通过
digits.images
获取。这是以 8 行 8 列形式表示的图像的原始数据。通过 "data",一张图像对应一个长度为 64 的一维 Numpy 数组;而 "images" 表示则包含形状为 (8, 8) 的二维 Numpy 数组。
Pythonprint("一个项目的形状: ", digits.data[0].shape) print("一个项目的数据类型: ", type(digits.data[0])) print("一个项目的形状: ", digits.images[0].shape) print("一个项目的数据类型: ", type(digits.images[0]))
输出:
一个项目的形状: (64,) 一个项目的数据类型: <class 'numpy.ndarray'> 一个项目的形状: (8, 8) 一个项目的数据类型: <class 'numpy.ndarray'>
让我们将数据可视化。这比我们上面使用的简单散点图稍微复杂一些,但我们可以很快完成。
Pythonimport matplotlib.pyplot as plt # 设置图表 fig = plt.figure(figsize=(6, 6)) # 图表大小(英寸) fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05) # 绘制数字:每个图像都是 8x8 像素 for i in range(64): ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[]) ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolation='nearest') # 用目标值标记图像 ax.text(0, 7, str(digits.target[i])) plt.show()
练习
练习 1
sklearn 中包含一个“葡萄酒数据集 (wine data set)”。
-
找到并加载此数据集。
-
您能找到它的描述吗?
-
类别的名称是什么?
-
特征是什么?
-
数据和带标签的数据在哪里?
练习 2
创建葡萄酒数据集中特征
ash
和color_intensity
的散点图。练习 3
创建葡萄酒数据集特征的散点矩阵。
练习 4
获取 Olivetti 人脸数据集并可视化这些人脸。
解决方案
练习 1 解决方案
加载“葡萄酒数据集”:
Pythonfrom sklearn import datasets wine = datasets.load_wine()
描述可以通过 "DESCR" 访问:
Pythonprint(wine.DESCR)
类别的名称和特征可以通过以下方式获取:
Pythonprint(wine.target_names) print(wine.feature_names)
输出:
['class_0' 'class_1' 'class_2'] ['alcohol', 'malic_acid', 'ash', 'alcalinity_of_ash', 'magnesium', 'total_phenols', 'flavanoids', 'nonflavanoid_phenols', 'proanthocyanins', 'color_intensity', 'hue', 'od280/od315_of_diluted_wines', 'proline']
数据和带标签的数据:
Pythondata = wine.data labelled_data = wine.target
练习 2 解决方案
Pythonfrom sklearn import datasets import matplotlib.pyplot as plt wine = datasets.load_wine() features = 'ash', 'color_intensity' features_index = [wine.feature_names.index(features[0]), wine.feature_names.index(features[1])] colors = ['blue', 'red', 'green'] for label, color in zip(range(len(wine.target_names)), colors): plt.scatter(wine.data[wine.target==label, features_index[0]], wine.data[wine.target==label, features_index[1]], label=wine.target_names[label], c=color) plt.xlabel(features[0]) plt.ylabel(features[1]) plt.legend(loc='upper left') plt.show()
练习 3 解决方案
Pythonimport pandas as pd from sklearn import datasets import matplotlib.pyplot as plt # 导入 matplotlib 以便显示图表 wine = datasets.load_wine() def rotate_labels(df, axes): """ 改变标签输出的旋转角度, y 轴标签水平,x 轴标签垂直 """ n = len(df.columns) for x in range(n): for y in range(n): # 获取子图的轴 ax = axes[x, y] # 使 x 轴名称垂直 ax.xaxis.label.set_rotation(90) # 使 y 轴名称水平 ax.yaxis.label.set_rotation(0) # 确保 y 轴名称在绘图区域之外 ax.yaxis.labelpad = 50 wine_df = pd.DataFrame(wine.data, columns=wine.feature_names) axs = pd.plotting.scatter_matrix(wine_df, c=wine.target, figsize=(8, 8), ) rotate_labels(wine_df, axs) plt.show() # 显示图表
练习 4 解决方案
Pythonfrom sklearn.datasets import fetch_olivetti_faces import numpy as np import matplotlib.pyplot as plt # 获取人脸数据 faces = fetch_olivetti_faces() faces.keys()
输出:
dict_keys(['data', 'images', 'target', 'DESCR'])
Pythonn_samples, n_features = faces.data.shape print((n_samples, n_features))
输出:
(400, 4096)
Pythonnp.sqrt(4096)
输出:
64.0
Pythonfaces.images.shape
输出:
(400, 64, 64)
Pythonfaces.data.shape
输出:
(400, 4096)
Pythonprint(np.all(faces.images.reshape((400, 4096)) == faces.data))
输出:
True
Python# 设置图表 fig = plt.figure(figsize=(6, 6)) # 图表大小(英寸) fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05) # 绘制人脸:每个图像是 64x64 像素 for i in range(64): ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[]) ax.imshow(faces.images[i], cmap=plt.cm.bone, interpolation='nearest') # 用目标值标记图像 ax.text(0, 7, str(faces.target[i])) plt.show()
更多数据集
sklearn 提供了许多其他数据集。如果您还需要更多,可以在维基百科的“机器学习研究数据集列表”中找到更多有用的信息。
We will have a closer look at one of these datasets. We look at the digits data set. We will load it first:
from sklearn.datasets import load_digits
digits = load_digits()
Again, we can get an overview of the available attributes by looking at the "keys":
digits.keys()
Output:dict_keys(['data', 'target', 'target_names', 'images', 'DESC
R'])
Let's have a look at the number of items and features:
n_samples, n_features = digits.data.shape
print((n_samples, n_features))
(1797, 64)
print(digits.data[0])
print(digits.target)
[ 0. 0. 5. 13. 9. 1. 0.
0.
0.
0. 13. 15. 10. 15.
5.
0.
0. 3.
15. 2. 0. 11. 8. 0. 0.
4. 12.
0.
0.
8.
8.
0.
0.
5.
8. 0.
0. 9. 8. 0. 0. 4. 11.
0.
1. 12.
7.
0.
0.
2. 14.
5. 1
0. 12.
0. 0. 0. 0. 6. 13. 10.
0.
0.
0.]
[0 1 2 ... 8 9 8]
The data is also available at digits.images. This is the raw data of the images in the form of 8 lines and 8
columns.
With "data" an image corresponds to a one-dimensional Numpy array with the length 64, and "images"
representation contains 2-dimensional numpy arrays with the shape (8, 8)
print("Shape of an item: ", digits.data[0].shape)
print("Data type of an item: ", type(digits.data[0]))
print("Shape of an item: ", digits.images[0].shape)
31
print("Data tpye of an item: ", type(digits.images[0]))
Shape of an item: (64,)
Data type of an item: <class 'numpy.ndarray'>
Shape of an item: (8, 8)
Data tpye of an item: <class 'numpy.ndarray'>
Let's visualize the data. It's little bit more involved than the simple scatter-plot we used above, but we can do it
rather quickly.
# set up the figure
fig = plt.figure(figsize=(6, 6)) # figure size in inches
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.0
5, wspace=0.05)
# plot the digits: each image is 8x8 pixels
for i in range(64):
ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolatio
n='nearest')
# label the image with the target value
ax.text(0, 7, str(digits.target[i]))
32
EXERCISES
EXERCISE 1
sklearn contains a "wine data set".
•
•
•
•
•
Find and load this data set
Can you find a description?
What are the names of the classes?
What are the features?
Where is the data and the labeled data?
EXERCISE 2:
Create a scatter plot of the features ash and color_intensity of the wine data set.
33
EXERCISE 3:
Create a scatter matrix of the features of the wine dataset.
EXERCISE 4:
Fetch the Olivetti faces dataset and visualize the faces.
SOLUTIONS
SOLUTION TO EXERCISE 1
Loading the "wine data set":
from sklearn import datasets
wine = datasets.load_wine()
The description can be accessed via "DESCR":
In [ ]:
print(wine.DESCR)
The names of the classes and the features can be retrieved like this:
print(wine.target_names)
print(wine.feature_names)
['class_0' 'class_1' 'class_2']
['alcohol', 'malic_acid', 'ash', 'alcalinity_of_ash', 'magnesiu
m', 'total_phenols', 'flavanoids', 'nonflavanoid_phenols', 'proant
hocyanins', 'color_intensity', 'hue', 'od280/od315_of_diluted_wine
s', 'proline']
data = wine.data
labelled_data = wine.target
SOLUTION TO EXERCISE 2:
from sklearn import datasets
import matplotlib.pyplot as plt
34
wine = datasets.load_wine()
features = 'ash', 'color_intensity'
features_index = [wine.feature_names.index(features[0]),
wine.feature_names.index(features[1])]
colors = ['blue', 'red', 'green']
for label, color in zip(range(len(wine.target_names)), colors):
plt.scatter(wine.data[wine.target==label, features_index[0]],
wine.data[wine.target==label, features_index[1]],
label=wine.target_names[label],
c=color)
plt.xlabel(features[0])
plt.ylabel(features[1])
plt.legend(loc='upper left')
plt.show()
SOLUTION TO EXERCISE 3:
import pandas as pd
from sklearn import datasets
wine = datasets.load_wine()
def rotate_labels(df, axes):
""" changing the rotation of the label output,
y labels horizontal and x labels vertical """
35
n = len(df.columns)
for x in range:
for y in range:
# to get the axis of subplots
ax = axs[x, y]
# to make x axis name vertical
ax.xaxis.label.set_rotation(90)
# to make y axis name horizontal
ax.yaxis.label.set_rotation(0)
# to make sure y axis names are outside the plot area
ax.yaxis.labelpad = 50
wine_df = pd.DataFrame(wine.data, columns=wine.feature_names)
axs = pd.plotting.scatter_matrix(wine_df,
c=wine.target,
figsize=(8, 8),
);
rotate_labels(wine_df, axs)
36
SOLUTION TO EXERCISE 4
from sklearn.datasets import fetch_olivetti_faces
# fetch the faces data
faces = fetch_olivetti_faces()
faces.keys()
Output:dict_keys(['data', 'images', 'target', 'DESCR'])
37
n_samples, n_features = faces.data.shape
print((n_samples, n_features))
(400, 4096)
np.sqrt(4096)
Output:64.0
faces.images.shape
Output400, 64, 64)
faces.data.shape
Output400, 4096)
print(np.all(faces.images.reshape((400, 4096)) == faces.data))
True
# set up the figure
fig = plt.figure(figsize=(6, 6)) # figure size in inches
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.0
5, wspace=0.05)
# plot the digits: each image is 8x8 pixels
for i in range(64):
ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
ax.imshow(faces.images[i], cmap=plt.cm.bone, interpolation='ne
arest')
# label the image with the target value
ax.text(0, 7, str(faces.target[i]))
38
FURTHER DATASETS
sklearn has many more datasets available. If you still need more, you will find more on this nice List of
datasets for machine-learning research at Wikipedia. -
-
现在我们将演示如何再次读取数据并将其重新分为数据和标签:
Pythonimport numpy as np # 从文件中加载数据 file_data = np.loadtxt("squirrels.txt") # 分离数据(所有列,除了最后一列) data = file_data[:, :-1] # 分离标签(最后一列) labels = file_data[:, 2:] # 将标签从二维列向量重新形状为一维数组 labels = labels.reshape((labels.shape[0]))
我们将数据文件命名为
squirrels.txt
,因为我们想象着一种生活在撒哈拉沙漠中的奇特动物。X 值代表这些动物的夜视能力,Y 值对应着毛皮的颜色,从沙色到黑色。我们有三种松鼠:0、1 和 2。(请注意,我们的松鼠是虚构的,与撒哈拉沙漠中真实的松鼠无关!)Pythonimport matplotlib.pyplot as plt colours = ('green', 'red', 'blue', 'magenta', 'yellow', 'cyan') n_classes = 3 # 类别数量 fig, ax = plt.subplots() # 遍历每个类别并绘制散点图 for n_class in range(0, n_classes): ax.scatter(data[labels==n_class, 0], data[labels==n_class, 1], c=colours[n_class], s=10, label=str(n_class)) ax.set(xlabel='夜视能力', ylabel='毛色 (沙色到黑色, 0到10)', title='撒哈拉虚拟松鼠') ax.legend(loc='upper right') plt.show()
训练人工数据
在下面的代码中,我们将训练我们的人工数据:
Pythonfrom sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn import metrics # 将数据分为训练集和测试集 data_sets = train_test_split(data, labels, train_size=0.8, # 训练集比例 test_size=0.2, # 测试集比例 random_state=42 # 随机种子,保证每次运行结果一致 ) train_data, test_data, train_labels, test_labels = data_sets # 导入模型 (K近邻分类器) # from sklearn.neighbors import KNeighborsClassifier # 已经在上面导入 # 创建分类器实例,设置 K 值为 8 knn = KNeighborsClassifier(n_neighbors=8) # 训练模型 knn.fit(train_data, train_labels) # 在测试集上进行预测 calculated_labels = knn.predict(test_data) print(calculated_labels)
输出:
array([2., 0., 1., 1., 0., 1., 2., 2., 2., 2., 0., 1., 0., 0., 1., 0., 1., 2., 0., 0., 1., 2., 1., 2., 2., 1., 2., 0., 0., 2., 0., 2., 2., 0., 0., 2., 0., 0., 0., 1., 0., 1., 1., 2., 0., 2., 1., 2., 1., 0., 2., 1., 1., 0., 1., 2., 1., 0., 0., 2., 1., 0., 1., 1., 0., 0., 0., 0., 0., 0., 0., 1., 1., 0., 1., 1., 1., 0., 1., 2., 1., 2., 0., 2., 1., 1., 0., 2., 2., 2., 0., 1., 1., 1., 2., 2., 0., 2., 2., 2., 2., 0., 0., 1., 1., 1., 2., 1., 1., 1., 0., 2., 1., 2., 0., 0., 1., 0., 1., 0., 2., 2., 2., 1., 1., 1., 0., 2., 1., 2., 2., 1., 2., 0., 2., 0., 0., 1., 0., 2., 2., 0., 0., 1., 2., 1., 2., 0., 0., 2., 2., 0., 0., 1., 2., 1., 2., 0., 0., 1., 2., 1., 0., 2., 2., 0., 2., 0., 0., 2., 1., 0., 0., 0., 0., 2., 2., 1., 0., 2., 2., 1., 2., 0., 1., 1., 1., 0., 1., 0., 1., 1., 2., 0., 2., 2., 1., 1., 1., 2.])
Python# 导入 metrics (评估指标) # from sklearn import metrics # 已经在上面导入 # 计算准确率 print("准确率:", metrics.accuracy_score(test_labels, calculated_labels))
输出:
准确率: 0.97
We will demonstrate now, how to read in the data again and how to split it into data and labels again:
file_data = np.loadtxt("squirrels.txt")
data = file_data[:,:-1]
labels = file_data[:,2:]
labels = labels.reshape((labels.shape[0]))
We had called the data file squirrels.txt , because we imagined a strange kind of animal living in the
Sahara desert. The x-values stand for the night vision capabilities of the animals and the y-values correspond
to the colour of the fur, going from sandish to black. We have three kinds of squirrels, 0, 1, and 2. (Be aware
that our squirrals are imaginary squirrels and have nothing to do with the real squirrels of the Sahara!)
import matplotlib.pyplot as plt
colours = ('green', 'red', 'blue', 'magenta', 'yellow', 'cyan')
n_classes = 3
fig, ax = plt.subplots()
for n_class in range(0, n_classes):
ax.scatter(data[labels==n_class, 0], data[labels==n_class,
1],
c=colours[n_class], s=10, label=str(n_class))
ax.set(xlabel='Night Vision',
ylabel='Fur color from sandish to black, 0 to 10 ',
title='Sahara Virtual Squirrel')
ax.legend(loc='upper right')
51
Output:<matplotlib.legend.Legend at 0x7f545b4d6340>
We will train our articifical data in the following code:
from sklearn.model_selection import train_test_split
data_sets = train_test_split(data,
labels,
train_size=0.8,
test_size=0.2,
random_state=42 # garantees same output fo
r every run
)
train_data, test_data, train_labels, test_labels = data_sets
# import model
from sklearn.neighbors import KNeighborsClassifier
# create classifier
knn = KNeighborsClassifier(n_neighbors=8)
# train
knn.fit(train_data,train_labels)
# test on test data:
calculated_labels = knn.predict(test_data)
calculated_labels
52
Output:array([2., 0., 1., 1., 0., 1., 2., 2., 2., 2., 0., 1., 0.,
0., 1., 0., 1.,
2., 0., 0., 1., 2., 1., 2., 2., 1., 2., 0., 0., 2.,
0., 2., 2., 0.,
0., 2., 0., 0., 0., 1., 0., 1., 1., 2., 0., 2., 1.,
2., 1., 0., 2.,
1., 1., 0., 1., 2., 1., 0., 0., 2., 1., 0., 1., 1.,
0., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 1., 1., 0., 1., 2., 1.,
2., 0., 2., 1.,
1., 0., 2., 2., 2., 0., 1., 1., 1., 2., 2., 0., 2.,
2., 2., 2., 0.,
0., 1., 1., 1., 2., 1., 1., 1., 0., 2., 1., 2., 0.,
0., 1., 0., 1.,
0., 2., 2., 2., 1., 1., 1., 0., 2., 1., 2., 2., 1.,
2., 0., 2., 0.,
0., 1., 0., 2., 2., 0., 0., 1., 2., 1., 2., 0., 0.,
2., 2., 0., 0.,
1., 2., 1., 2., 0., 0., 1., 2., 1., 0., 2., 2., 0.,
2., 0., 0., 2.,
1., 0., 0., 0., 0., 2., 2., 1., 0., 2., 2., 1., 2.,
0., 1., 1., 1.,
0., 1., 0., 1., 1., 2., 0., 2., 2., 1., 1., 1., 2.])
from sklearn import metrics
print("Accuracy:", metrics.accuracy_score(test_labels, calculate
d_labels))
Accuracy: 0.97 -
首先,代码使用
sklearn.datasets.make_moons
函数生成了一个“月亮”形状的二维数据集:Pythonimport numpy as np import sklearn.datasets as ds data, labels = ds.make_moons(n_samples=150, shuffle=True, noise=0.19, random_state=None)
-
n_samples=150
:生成150个数据点。 -
shuffle=True
:打乱数据。 -
noise=0.19
:数据中加入的噪声量。
接下来,对数据进行了平移,使得第一个特征(X轴)的最小值变为0:
Pythondata += np.array([-np.ndarray.min(data[:,0]), -np.ndarray.min(data[:,1])]) np.ndarray.min(data[:,0]), np.ndarray.min(data[:,1]) # 输出: (0.0, 0.34649342272719386)
这确保了数据集的某个特定坐标轴的起始点。
然后,使用
matplotlib.pyplot
将生成的数据可视化:Pythonimport matplotlib.pyplot as plt fig, ax = plt.subplots() ax.scatter(data[labels==0, 0], data[labels==0, 1], c='orange', s=40, label='oranges') ax.scatter(data[labels==1, 0], data[labels==1, 1], c='blue', s=40, label='blues') ax.set(xlabel='X', ylabel='Y', title='Moons') #ax.legend(loc='upper right');
-
将
labels
为0的数据点标记为橙色,labels
为1的数据点标记为蓝色。 -
设置了X轴、Y轴标签和图表标题。
数据缩放
接着,文本介绍了一个将数据从一个范围 [min,max] 缩放到另一个范围 [a,b] 的公式:
这个公式用于将数据点的X和Y坐标转换到新的范围:
Pythonmin_x_new, max_x_new = 33, 88 min_y_new, max_y_new = 12, 20 data, labels = ds.make_moons(n_samples=100, shuffle=True, noise=0.05, random_state=None) min_x, min_y = np.ndarray.min(data[:,0]), np.ndarray.min(data[:,1]) max_x, max_y = np.ndarray.max(data[:,0]), np.ndarray.max(data[:,1]) data -= np.array([min_x, min_y]) # 1. 平移数据,使最小值变为0 data *= np.array([(max_x_new - min_x_new) / (max_x - min_x), (max_y_new - min_y_new) / (max_y - min_y)]) # 2. 缩放数据到新范围的比例 data += np.array([min_x_new, min_y_new]) # 3. 平移数据到新范围的最小值 # 输出转换后的前6个数据点: # Output:array([[71.14479608, 12.28919998], ...])
这一系列操作实现了数据的最小-最大归一化。
scale_data
函数为了方便地进行数据缩放,定义了一个
scale_data
函数:Pythondef scale_data(data, new_limits, inplace=False): if not inplace: data = data.copy() # 如果inplace为False,则复制数据,避免修改原始数据 min_x, min_y = np.ndarray.min(data[:,0]), np.ndarray.min(data[:,1]) max_x, max_y = np.ndarray.max(data[:,0]), np.ndarray.max(data[:,1]) min_x_new, max_x_new = new_limits[0] min_y_new, max_y_new = new_limits[1] data -= np.array([min_x, min_y]) data *= np.array([(max_x_new - min_x_new) / (max_x - min_x), (max_y_new - min_y_new) / (max_y - min_y)]) data += np.array([min_x_new, min_y_new]) if inplace: return None # 如果inplace为True,直接修改传入的数据,返回None else: return data # 否则返回缩放后的新数据
该函数接受数据、新的范围
new_limits
和一个inplace
参数。如果inplace
为True
,则直接修改原始数据;否则返回一个缩放后的新副本。接着,使用这个函数对“月亮”数据集进行缩放并可视化:
Pythondata, labels = ds.make_moons(n_samples=100, shuffle=True, noise=0.05, random_state=None) scale_data(data, [(1, 4), (3, 8)], inplace=True) # 将数据缩放到 X 轴范围 [1, 4] 和 Y 轴范围 [3, 8] # 输出缩放后的前10个数据点: # Output:array([[1.19312571, 6.70797983], ...]) fig, ax = plt.subplots() ax.scatter(data[labels==0, 0], data[labels==0, 1], c='orange', s=40, label='oranges') ax.scatter(data[labels==1, 0], data[labels==1, 1], c='blue', s=40, label='blues') ax.set(xlabel='X', ylabel='Y', title='moons') ax.legend(loc='upper right');
Circles 数据集可视化
代码随后展示了如何生成和可视化“圆形”数据集:
Pythonimport sklearn.datasets as ds data, labels = ds.make_circles(n_samples=100, shuffle=True, noise=0.05, random_state=None) fig, ax = plt.subplots() ax.scatter(data[labels==0, 0], data[labels==0, 1], c='orange', s=40, label='oranges') ax.scatter(data[labels==1, 0], data[labels==1, 1], c='blue', s=40, label='blues') ax.set(xlabel='X', ylabel='Y', title='circles') ax.legend(loc='upper right')
不同类型的分类数据集
接下来,代码演示了
sklearn.datasets
中其他用于生成分类数据集的函数,并进行可视化:Pythonimport matplotlib.pyplot as plt from sklearn.datasets import make_classification, make_blobs, make_gaussian_quantiles plt.figure(figsize=(8, 8)) plt.subplots_adjust(bottom=.05, top=.9, left=.05, right=.95) # 1. 两个特征,一个信息性特征,每个类别一个簇 plt.subplot(321) plt.title("One informative feature, one cluster per class", fontsize='small') X1, Y1 = make_classification(n_features=2, n_redundant=0, n_informative=1, n_clusters_per_class=1) plt.scatter(X1[:, 0], X1[:, 1], marker='o', c=Y1, s=25, edgecolor='k') # 2. 两个特征,两个信息性特征,每个类别一个簇 plt.subplot(322) plt.title("Two informative features, one cluster per class", fontsize='small') X1, Y1 = make_classification(n_features=2, n_redundant=0, n_informative=2, n_clusters_per_class=1) plt.scatter(X1[:, 0], X1[:, 1], marker='o', c=Y1, s=25, edgecolor='k') # 3. 两个特征,两个信息性特征,每个类别两个簇 plt.subplot(323) plt.title("Two informative features, two clusters per class", fontsize='small') X2, Y2 = make_classification(n_features=2, n_redundant=0, n_informative=2) plt.scatter(X2[:, 0], X2[:, 1], marker='o', c=Y2, s=25, edgecolor='k') # 4. 多类别,两个信息性特征,一个簇 plt.subplot(324) plt.title("Multi-class, two informative features, one cluster", fontsize='small') X1, Y1 = make_classification(n_features=2, n_redundant=0, n_informative=2, n_clusters_per_class=1, n_classes=3) plt.scatter(X1[:, 0], X1[:, 1], marker='o', c=Y1, s=25, edgecolor='k') # 5. 高斯分布分为三个分位数 plt.subplot(325) plt.title("Gaussian divided into three quantiles", fontsize='small') X1, Y1 = make_gaussian_quantiles(n_features=2, n_classes=3) plt.scatter(X1[:, 0], X1[:, 1], marker='o', c=Y1, s=25, edgecolor='k') plt.show()
这部分代码展示了
make_classification
和make_gaussian_quantiles
如何生成不同复杂度、不同类别数量和不同特征结构的数据集,用于机器学习任务的测试。
练习
这部分提出了三个练习,要求用户创建满足特定条件的数据集。
练习 1
创建一个可以被感知机(不带偏置节点)分离的两个测试集。
感知机不带偏置节点意味着决策边界必须通过原点。
练习 2
创建两个不能被通过原点的分割线分离的测试集。
练习 3
创建一个包含“Tiger”、“Lion”、“Penguin”、“Dolphin”和“Python”五个类别的数据集,其分布类似于给定的图示。
练习解答
这部分提供了上述练习的解决方案。
练习 1 解决方案
使用
make_blobs
创建两个簇,它们分别位于相对的象限,使得通过原点的直线可以将其分离:Pythonfrom sklearn.datasets import make_blobs data, labels = make_blobs(n_samples=100, cluster_std = 0.5, centers=[[1, 4] ,[4, 1]], random_state=1) # 簇中心设置为 [1, 4] 和 [4, 1],这样一条穿过原点的线可以分隔它们。 fig, ax = plt.subplots() colours = ["orange", "green"] label_name = ["Tigers", "Lions"] for label in range(0, 2): ax.scatter(data[labels==label, 0], data[labels==label, 1], c=colours[label], s=40, label=label_name[label]) ax.set(xlabel='X', ylabel='Y', title='dataset') ax.legend(loc='upper right')
练习 2 解决方案
创建两个不能被通过原点的分割线分离的簇。例如,将两个簇都放置在同一个象限,或者一个簇围绕原点,另一个在外部。这里的解决方案是将两个簇放置在第一象限:
Pythonfrom sklearn.datasets import make_blobs data, labels = make_blobs(n_samples=100, cluster_std = 0.5, centers=[[2, 2] ,[4, 4]], random_state=1) # 簇中心设置为 [2, 2] 和 [4, 4],都在第一象限,无法被通过原点的线分离。 fig, ax = plt.subplots() colours = ["orange", "green"] label_name = ["label0", "label1"] for label in range(0, 2): ax.scatter(data[labels==label, 0], data[labels==label, 1], c=colours[label], s=40, label=label_name[label]) ax.set(xlabel='X', ylabel='Y', title='dataset') ax.legend(loc='upper right')
练习 3 解决方案
结合
make_circles
和make_blobs
来创建五个类别的数据集,模拟复杂的分布:Pythonimport sklearn.datasets as ds from sklearn.datasets import make_blobs import numpy as np # 生成第一个圆形数据集 (作为两个类别) data, labels = ds.make_circles(n_samples=100, shuffle=True, noise=0.05, random_state=42) # 生成第二个 blob 数据集 (作为三个类别) centers = [[3, 4], [5, 3], [4.5, 6]] data2, labels2 = make_blobs(n_samples=100, cluster_std = 0.5, centers=centers, random_state=1) # 调整 labels2 的标签,使其与 labels 不重叠,从2开始 for i in range(len(centers)-1, -1, -1): labels2[labels2==0+i] = i+2 # print(labels2) # 输出调整后的 labels2 数组 # 合并两个数据集的标签 labels = np.concatenate([labels, labels2]) # 对第一个圆形数据集进行缩放和平移,使其与 blob 数据集结合时位置合适 data = data * [1.2, 1.8] + [3, 4] # 合并两个数据集的数据 data = np.concatenate([data, data2], axis=0) fig, ax = plt.subplots() colours = ["orange", "blue", "magenta", "yellow", "green"] label_name = ["Tiger", "Lion", "Penguin", "Dolphin", "Python"] for label in range(0, len(centers)+2): # 遍历所有5个类别 ax.scatter(data[labels==label, 0], data[labels==label, 1], c=colours[label], s=40, label=label_name[label]) ax.set(xlabel='X', ylabel='Y', title='dataset') ax.legend(loc='upper right')
这个解决方案通过
make_circles
创建了内外的两个圈,然后通过make_blobs
创建了三个离散的簇,并将它们合并在一起,形成了五个不同类别的数据集。
import numpy as np
import sklearn.datasets as ds
data, labels = ds.make_moons(n_samples=150,
shuffle=True,
noise=0.19,
random_state=None)
data += np.array(-np.ndarray.min(data[:,0]),
-np.ndarray.min(data[:,1]))
np.ndarray.min(data[:,0]), np.ndarray.min(data[:,1])
Output0.0, 0.34649342272719386)
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.scatter(data[labels==0, 0], data[labels==0, 1],
c='orange', s=40, label='oranges')
ax.scatter(data[labels==1, 0], data[labels==1, 1],
c='blue', s=40, label='blues')
ax.set(xlabel='X',
ylabel='Y',
title='Moons')
#ax.legend(loc='upper right');
54
Output:[Text(0.5, 0, 'X'), Text(0, 0.5, 'Y'), Text(0.5, 1.0, 'Moon
s')]
We want to scale values that are in a range [min, max] in a range [a, b] .
(b − a) ⋅ (x − min)
f(x) =
+ a
max − min
We now use this formula to transform both the X and Y coordinates of data into other ranges:
min_x_new, max_x_new = 33, 88
min_y_new, max_y_new = 12, 20
data, labels = ds.make_moons(n_samples=100,
shuffle=True,
noise=0.05,
random_state=None)
min_x, min_y = np.ndarray.min(data[:,0]), np.ndarray.min(dat
a[:,1])
max_x, max_y = np.ndarray.max(data[:,0]), np.ndarray.max(dat
a[:,1])
#data -= np.array([min_x, 0])
#data *= np.array([(max_x_new - min_x_new) / (max_x - min_x), 1])
#data += np.array([min_x_new, 0])
#data -= np.array([0, min_y])
#data *= np.array([1, (max_y_new - min_y_new) / (max_y - min_y)])
55
#data += np.array([0, min_y_new])
data -= np.array([min_x, min_y])
data *= np.array([(max_x_new - min_x_new) / (max_x - min_x), (ma
x_y_new - min_y_new) / (max_y - min_y)])
data += np.array([min_x_new, min_y_new])
#np.ndarray.min(data[:,0]), np.ndarray.max(data[:,0])
data[:6]
Output:array([[71.14479608, 12.28919998],
[62.16584307, 18.75442981],
[61.02613211, 12.80794358],
[64.30752046, 12.32563839],
[81.41469127, 13.64613406],
[82.03929032, 13.63156545]])
def scale_data(data, new_limits, inplace=False ):
if not inplace:
data = data.copy()
min_x, min_y = np.ndarray.min(data[:,0]), np.ndarray.min(dat
a[:,1])
max_x, max_y = np.ndarray.max(data[:,0]), np.ndarray.max(dat
a[:,1])
min_x_new, max_x_new = new_limits[0]
min_y_new, max_y_new = new_limits[1]
data -= np.array([min_x, min_y])
data *= np.array([(max_x_new - min_x_new) / (max_x - min_x),
(max_y_new - min_y_new) / (max_y - min_y)])
data += np.array([min_x_new, min_y_new])
if inplace:
return None
else:
return data
data, labels = ds.make_moons(n_samples=100,
shuffle=True,
noise=0.05,
random_state=None)
scale_data(data, [(1, 4), (3, 8)], inplace=True)
56
data[:10]
Output:array([[1.19312571, 6.70797983],
[2.74306138, 6.74830445],
[1.15255757, 6.31893824],
[1.03927303, 4.83714182],
[2.91313352, 6.44139267],
[2.13227292, 5.120716 ],
[2.65590196, 3.49417953],
[2.98349928, 5.02232383],
[3.35660593, 3.34679462],
[2.15813861, 4.8036458 ]])
fig, ax = plt.subplots()
ax.scatter(data[labels==0, 0], data[labels==0, 1],
c='orange', s=40, label='oranges')
ax.scatter(data[labels==1, 0], data[labels==1, 1],
c='blue', s=40, label='blues')
ax.set(xlabel='X',
ylabel='Y',
title='moons')
ax.legend(loc='upper right');
import sklearn.datasets as ds
data, labels = ds.make_circles(n_samples=100,
shuffle=True,
57
fig, ax = plt.subplots()
noise=0.05,
random_state=None)
ax.scatter(data[labels==0, 0], data[labels==0, 1],
c='orange', s=40, label='oranges')
ax.scatter(data[labels==1, 0], data[labels==1, 1],
c='blue', s=40, label='blues')
ax.set(xlabel='X',
ylabel='Y',
title='circles')
ax.legend(loc='upper right')
Output:<matplotlib.legend.Legend at 0x7f54588c2e20>
print(__doc__)
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.datasets import make_blobs
from sklearn.datasets import make_gaussian_quantiles
plt.figure(figsize=(8, 8))
plt.subplots_adjust(bottom=.05, top=.9, left=.05, right=.95)
58
plt.subplot(321)
plt.title("One informative feature, one cluster per class", fontsi
ze='small')
X1, Y1 = make_classification(n_features=2, n_redundant=0, n_inform
ative=1,
n_clusters_per_class=1)
plt.scatter(X1[:, 0], X1[:, 1], marker='o', c=Y1,
s=25, edgecolor='k')
plt.subplot(322)
plt.title("Two informative features, one cluster per class", fonts
ize='small')
X1, Y1 = make_classification(n_features=2, n_redundant=0, n_inform
ative=2,
n_clusters_per_class=1)
plt.scatter(X1[:, 0], X1[:, 1], marker='o', c=Y1,
s=25, edgecolor='k')
plt.subplot(323)
plt.title("Two informative features, two clusters per class",
fontsize='small')
X2, Y2 = make_classification(n_features=2,
n_redundant=0,
n_informative=2)
plt.scatter(X2[:, 0], X2[:, 1], marker='o', c=Y2,
s=25, edgecolor='k')
plt.subplot(324)
plt.title("Multi-class, two informative features, one cluster",
fontsize='small')
X1, Y1 = make_classification(n_features=2,
n_redundant=0,
n_informative=2,
n_clusters_per_class=1,
n_classes=3)
plt.scatter(X1[:, 0], X1[:, 1], marker='o', c=Y1,
s=25, edgecolor='k')
plt.subplot(325)
plt.title("Gaussian divided into three quantiles", fontsize='smal
l')
X1, Y1 = make_gaussian_quantiles(n_features=2, n_classes=3)
plt.scatter(X1[:, 0], X1[:, 1], marker='o', c=Y1,
s=25, edgecolor='k')
59
plt.show()
Automatically created module for IPython interactive environment
EXERCISES
EXERCISE 1
Create two testsets which are separable with a perceptron without a bias node.
EXERCISE 2
Create two testsets which are not separable with a dividing line going through the origin.
60
EXERCISE 3
Create a dataset with five classes "Tiger", "Lion", "Penguin", "Dolphin", and "Python". The sets should look
similar to the following diagram:
SOLUTIONS
SOLUTION TO EXERCISE 1
data, labels = make_blobs(n_samples=100,
cluster_std = 0.5,
centers=[[1, 4] ,[4, 1]],
random_state=1)
fig, ax = plt.subplots()
colours = ["orange", "green"]
label_name = ["Tigers", "Lions"]
for label in range(0, 2):
ax.scatter(data[labels==label, 0], data[labels==label, 1],
c=colours[label], s=40, label=label_name[label])
ax.set(xlabel='X',
ylabel='Y',
title='dataset')
61
ax.legend(loc='upper right')
Output:<matplotlib.legend.Legend at 0x7f788afb2c40>
SOLUTION TO EXERCISE 2
data, labels = make_blobs(n_samples=100,
cluster_std = 0.5,
centers=[[2, 2] ,[4, 4]],
random_state=1)
fig, ax = plt.subplots()
colours = ["orange", "green"]
label_name = ["label0", "label1"]
for label in range(0, 2):
ax.scatter(data[labels==label, 0], data[labels==label, 1],
c=colours[label], s=40, label=label_name[label])
ax.set(xlabel='X',
ylabel='Y',
title='dataset')
ax.legend(loc='upper right')
62
Output:<matplotlib.legend.Legend at 0x7f788af8eac0>
SOLUTION TO EXERCISE 3
import sklearn.datasets as ds
data, labels = ds.make_circles(n_samples=100,
shuffle=True,
noise=0.05,
random_state=42)
centers = [[3, 4], [5, 3], [4.5, 6]]
data2, labels2 = make_blobs(n_samples=100,
cluster_std = 0.5,
centers=centers,
random_state=1)
for i in range(len(centers)-1, -1, -1):
labels2[labels2==0+i] = i+2
print(labels2)
labels = np.concatenate([labels, labels2])
data = data * [1.2, 1.8] + [3, 4]
data = np.concatenate([data, data2], axis=0)
63
[2 4 4 3 4 4 3 3 2 4 4 2 4 4 3 4 2 4 4 4 4 2 2 4 4 3 2 2 3 2 2 3
2 3 3 3 3
3 4 3 3 2 3 3 3 2 2 2 2 3 4 4 4 2 4 3 3 2 2 3 4 4 3 3 4 2 4 2 4
3 3 4 2 2
3 4 4 2 3 2 3 3 4 2 2 2 2 3 2 4 2 2 3 3 4 4 2 2 4 3]
fig, ax = plt.subplots()
colours = ["orange", "blue", "magenta", "yellow", "green"]
label_name = ["Tiger", "Lion", "Penguin", "Dolphin", "Python"]
for label in range(0, len(centers)+2):
ax.scatter(data[labels==label, 0], data[labels==label, 1],
c=colours[label], s=40, label=label_name[label])
ax.set(xlabel='X',
ylabel='Y',
title='dataset')
ax.legend(loc='upper right')
Output:<matplotlib.legend.Legend at 0x7f788b1d42b0> -
-
“告诉我你的朋友是谁,我就能告诉你你是谁?”
K 近邻分类器的概念再简单不过了。
这是一句古老的谚语,在许多语言和文化中都能找到。圣经中也以其他方式提到了它:“与智慧人同行,必得智慧;与愚昧人作伴,必受亏损。”(箴言 13:20)
这意味着 K 近邻分类器的概念是我们日常生活和判断的一部分:想象你遇到一群人,他们都非常年轻、时尚且热爱运动。他们谈论着不在场的他们的朋友本。那么,你对本的印象是什么?没错,你也会把他想象成一个年轻、时尚且热爱运动的人。
如果你得知本住在一个人们投票倾向保守、平均年收入超过 20 万美元的社区?而且他的两个邻居甚至每年挣得超过 30 万美元?你对本会怎么想?很可能你不会认为他是个失败者,甚至可能会怀疑他也是个保守派?
近邻分类的原理在于找到预定义数量的(即“k”个)训练样本,这些样本在距离上与待分类的新样本最接近。新样本的标签将由这些近邻决定。K 近邻分类器有一个用户定义的固定常数,用于确定需要找到的近邻数量。还有基于半径的近邻学习算法,它们根据点的局部密度,在固定半径内包含所有样本,从而具有可变数量的近邻。距离通常可以是任何度量:标准欧几里得距离是最常见的选择。基于近邻的方法被称为非泛化机器学习方法,因为它们只是简单地“记住”了所有训练数据。分类可以通过未知样本的最近邻的多数投票来计算。
K-NN 算法是所有机器学习算法中最简单的之一,但尽管它很简单,它在大量的分类和回归问题中都取得了相当大的成功,例如字符识别或图像分析。
现在让我们稍微深入一些数学层面:
正如数据准备一章中所解释的,我们需要带标签的学习数据和测试数据。然而,与其他分类器不同,纯粹的近邻分类器不做任何学习,而是将所谓的**学习集(LS)**作为分类器的基本组成部分。K 近邻分类器(kNN)直接作用于学习到的样本,而不是像其他分类方法那样创建规则。
近邻算法:
给定一组类别 LS:
,也称为类,例如 {"男", "女"}。还有一个由带标签的实例组成的学习集由于带标签的项少于类别数没有意义,我们可以假定n > m ,在大多数情况下甚至 n ⋙ m (n 远大于 m)。
分类的任务在于将一个类别或类 c 分配给任意实例 o。
为此,我们必须区分两种情况:
-
情况 1:
实例 o 是 LS 的一个元素,即存在一个元组 (o, c) ∈ LS
在这种情况下,我们将使用类别 c 作为分类结果。
- 情况 2:
我们现在假设 o 不在 LS 中,或者更确切地说:
∀c ∈ C, (o, c) ∉ LS
将 o 与 LS 中的所有实例进行比较。比较时使用距离度量 d。
我们确定 o 的 k 个最近邻,即距离最小的项。
k 是一个用户定义的常数,一个通常较小的正整数。
数字 k 通常选择为 LS 的平方根,即训练数据集中点的总数。
为了确定 k 个最近邻,我们按以下方式重新排序 LS:
这样对于所有
都成立。
k 个最近邻的集合 N_k 由此排序的前 k 个元素组成,即:
在这个最近邻集合 N_k 中最常见的类别将被分配给实例 o。如果没有唯一的最常见类别,我们则任意选择其中一个。
没有通用的方法来定义“k”的最佳值。这个值取决于数据。通常我们可以说,增加“k”会减少噪声,但另一方面会使边界不那么清晰。
K 近邻分类器的算法是所有机器学习算法中最简单的之一。K-NN 是一种基于实例的学习,或者说是惰性学习。在机器学习中,惰性学习被理解为一种学习方法,其中训练数据的泛化被推迟到系统发出查询时。另一方面,我们有急切学习,其中系统通常在接收查询之前泛化训练数据。换句话说:函数只在局部近似,所有的计算都在实际执行分类时进行。
下图以简单的方式展示了近邻分类器的工作原理。拼图块是未知的。为了找出它可能是什么动物,我们必须找到它的邻居。如果 k=1,唯一的邻居是猫,在这种情况下,我们假设这个拼图块也应该是一只猫。如果 k=4,最近邻包含一只鸡和三只猫。在这种情况下,同样可以放心地假设我们所讨论的对象应该是一只猫。
从零开始实现 K 近邻分类器
准备数据集
在我们实际开始编写近邻分类器之前,我们需要考虑数据,即学习集和测试集。我们将使用
sklearn
模块的数据集提供的“iris”数据集。该数据集包含来自三种鸢尾花物种各 50 个样本:
-
鸢尾花(Iris setosa)
-
维吉尼亚鸢尾(Iris virginica)
-
变色鸢尾(Iris versicolor)
每个样本测量了四个特征:萼片和花瓣的长度和宽度,单位为厘米。
Pythonimport numpy as np from sklearn import datasets iris = datasets.load_iris()
Pythondata = iris.data labels = iris.target for i in [0, 79, 99, 101]: print(f"index: {i:3}, features: {data[i]}, label: {labels[i]}")
index: 0, features: [5.1 3.5 1.4 0.2], label: 0 index: 79, features: [5.7 2.6 3.5 1. ], label: 1 index: 99, features: [5.7 2.8 4.1 1.3], label: 1 index: 101, features: [5.8 2.7 5.1 1.9], label: 2
我们从上述集合中创建一个学习集。我们使用
np.random.permutation
随机分割数据。Python# 播种只对网站需要 # 以便值始终相等: np.random.seed(42) indices = np.random.permutation(len(data)) n_training_samples = 12 learn_data = data[indices[:-n_training_samples]] learn_labels = labels[indices[:-n_training_samples]] test_data = data[indices[-n_training_samples:]] test_labels = labels[indices[-n_training_samples:]] print("The first samples of our learn set:") print(f"{'index':7s}{'data':20s}{'label':3s}") for i in range(5): print(f"{i:4d} {learn_data[i]} {learn_labels[i]:3}") print("The first samples of our test set:") print(f"{'index':7s}{'data':20s}{'label':3s}") for i in range(5): print(f"{i:4d} {test_data[i]} {test_labels[i]:3}") # 修正:这里应该是test_data和test_labels
The first samples of our learn set: index data label 0 [6.1 2.8 4.7 1.2] 1 1 [5.7 3.8 1.7 0.3] 0 2 [7.7 2.6 6.9 2.3] 2 3 [6. 2.9 4.5 1.5] 1 4 [6.8 2.8 4.8 1.4] 1 The first samples of our test set: index data label 0 [5.7 2.8 4.1 1.3] 1 1 [6.5 3. 5.5 1.8] 2 2 [6.3 2.3 4.4 1.3] 1 3 [6.4 2.9 4.3 1.3] 1 4 [5.6 2.8 4.9 2. ] 2
以下代码仅用于可视化我们的学习集数据。我们的数据每项鸢尾花包含四个值,因此我们将通过将第三个和第四个值相加来将数据减少到三个值。这样,我们就能在 3 维空间中描绘数据:
Python#%matplotlib widget import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D X = [] for iclass in range(3): X.append([[], [], []]) for i in range(len(learn_data)): if learn_labels[i] == iclass: X[iclass][0].append(learn_data[i][0]) X[iclass][1].append(learn_data[i][1]) X[iclass][2].append(sum(learn_data[i][2:])) colours = ("r", "g", "y") # 修正:原始代码中此处使用了colours = ("r", "b"),但实际上有三类,需要三个颜色 fig = plt.figure() ax = fig.add_subplot(111, projection='3d') for iclass in range(3): ax.scatter(X[iclass][0], X[iclass][1], X[iclass][2], c=colours[iclass]) plt.show()
距离度量
我们已经详细提到,我们计算样本点与待分类对象之间的距离。为了计算这些距离,我们需要一个距离函数。
在 n 维向量空间中,通常使用以下三种距离度量之一:
-
欧几里得距离 (Euclidean Distance)
欧几里得距离衡量平面或 3 维空间中两点 x 和 y 之间连接这两个点的线段的长度。它可以根据点的笛卡尔坐标使用勾股定理计算,因此偶尔也称为勾股距离。通用公式是:
-
曼哈顿距离 (Manhattan Distance)
它定义为 x 和 y 坐标之间差值的绝对值之和:
-
闵可夫斯基距离 (Minkowski Distance)
闵可夫斯基距离将欧几里得距离和曼哈顿距离概括为一种距离度量。如果我们将以下公式中的参数 p 设置为 1,我们得到曼哈顿距离;使用值 2 则得到欧几里得距离:
下图可视化了欧几里得距离和曼哈顿距离:
蓝线表示绿点和红点之间的欧几里得距离。另外,你也可以沿着橙色、绿色或黄色的线从绿点移动到红点。这些线对应于曼哈顿距离。它们的长度是相等的。
确定近邻
为了确定两个实例之间的相似性,我们将使用欧几里得距离。
我们可以使用
np.linalg
模块的norm
函数计算欧几里得距离:Pythondef distance(instance1, instance2): """ 计算两个实例之间的欧几里得距离 """ return np.linalg.norm(np.subtract(instance1, instance2)) print(distance([3, 5], [1, 1])) print(distance(learn_data[3], learn_data[44]))
4.47213595499958 3.4190641994557516
get_neighbors
函数返回一个包含 k 个邻居的列表,这些邻居与实例test_instance
最接近:Pythondef get_neighbors(training_set, labels, test_instance, k, distance): """ get_neighbors 计算实例 'test_instance' 的 k 个最近邻的列表。 函数返回一个包含 k 个 3 元组的列表。 每个 3 元组由 (index, dist, label) 组成 其中 index 是 training_set 中的索引, dist 是 test_instance 和 training_set[index] 实例之间的距离 distance 是对用于计算距离的函数的引用 """ distances = [] for index in range(len(training_set)): dist = distance(test_instance, training_set[index]) distances.append((training_set[index], dist, labels[index])) distances.sort(key=lambda x: x[1]) # 按距离排序 neighbors = distances[:k] # 取前 k 个 return neighbors
我们将使用 Iris 样本测试该函数:
Pythonfor i in range(5): neighbors = get_neighbors(learn_data, learn_labels, test_data[i], 3, distance=distance) print("Index: ",i,'\n', "Testset Data: ",test_data[i],'\n', "Testset Label: ",test_labels[i],'\n', "Neighbors: ",neighbors,'\n')
Index: 0 Testset Data: [5.7 2.8 4.1 1.3] Testset Label: 1 Neighbors: [(array([5.7, 2.9, 4.2, 1.3]), 0.14142135623730995, 1), (array([5.6, 2.7, 4.2, 1.3]), 0.17320508075688815, 1), (array([5.6, 3., 4.1, 1.3]), 0.22360679774997935, 1)] Index: 1 Testset Data: [6.5 3. 5.5 1.8] Testset Label: 2 Neighbors: [(array([6.4, 3.1, 5.5, 1.8]), 0.1414213562373093, 2), (array([6.3, 2.9, 5.6, 1.8]), 0.24494897427831783, 2), (array([6.5, 3., 5.2, 2.]), 0.3605551275463988, 2)] Index: 2 Testset Data: [6.3 2.3 4.4 1.3] Testset Label: 1 Neighbors: [(array([6.2, 2.2, 4.5, 1.5]), 0.2645751311064586, 1), (array([6.3, 2.5, 4.9, 1.5]), 0.574456264653803, 1), (array([6., 2.2, 4., 1.]), 0.5916079783099617, 1)] Index: 3 Testset Data: [6.4 2.9 4.3 1.3] Testset Label: 1 Neighbors: [(array([6.2, 2.9, 4.3, 1.3]), 0.20000000000000018, 1), (array([6.6, 3., 4.4, 1.4]), 0.2645751311064587, 1), (array([6.6, 2.9, 4.6, 1.3]), 0.3605551275463984, 1)] Index: 4 Testset Data: [5.6 2.8 4.9 2. ] Testset Label: 2 Neighbors: [(array([5.8, 2.7, 5.1, 1.9]), 0.3162277660168375, 2), (array([5.8, 2.7, 5.1, 1.9]), 0.3162277660168375, 2), (array([5.7, 2.5, 5., 2.]), 0.33166247903553986, 2)]
投票以获得单一结果
我们现在将编写一个
vote
函数。这个函数使用collections
模块中的Counter
类来计算实例列表(当然是邻居)中各个类别的数量。vote
函数返回最常见的类别:Pythonfrom collections import Counter def vote(neighbors): class_counter = Counter() for neighbor in neighbors: class_counter[neighbor[2]] += 1 return class_counter.most_common(1)[0][0]
我们将在训练样本上测试
vote
:Pythonfor i in range(n_training_samples): neighbors = get_neighbors(learn_data, learn_labels, test_data[i], 3, distance=distance) print("index: ", i, ", result of vote: ", vote(neighbors), ", label: ", test_labels[i], ", data: ", test_data[i])
index: 0 , result of vote: 1 , label: 1 , data: [5.7 2.8 4.1 1.3] index: 1 , result of vote: 2 , label: 2 , data: [6.5 3. 5.5 1.8] index: 2 , result of vote: 1 , label: 1 , data: [6.3 2.3 4.4 1.3] index: 3 , result of vote: 1 , label: 1 , data: [6.4 2.9 4.3 1.3] index: 4 , result of vote: 2 , label: 2 , data: [5.6 2.8 4.9 2. ] index: 5 , result of vote: 2 , label: 2 , data: [5.9 3. 5.1 1.8] index: 6 , result of vote: 0 , label: 0 , data: [5.4 3.4 1.7 0.2] index: 7 , result of vote: 1 , label: 1 , data: [6.1 2.8 4. 1.3] index: 8 , result of vote: 1 , label: 2 , data: [4.9 2.5 4.5 1.7] index: 9 , result of vote: 0 , label: 0 , data: [5.8 4. 1.2 0.2] index: 10 , result of vote: 1 , label: 1 , data: [5.8 2.6 4. 1.2] index: 11 , result of vote: 2 , label: 2 , data: [7.1 3. 5.9 2.1]
我们可以看到,除了索引为 8 的项外,预测结果与带标签的结果一致。
vote_prob
函数类似于vote
,但它返回类名和该类的概率:Pythondef vote_prob(neighbors): class_counter = Counter() for neighbor in neighbors: class_counter[neighbor[2]] += 1 labels, votes = zip(*class_counter.most_common()) winner = class_counter.most_common(1)[0][0] votes4winner = class_counter.most_common(1)[0][1] return winner, votes4winner/sum(votes)
Pythonfor i in range(n_training_samples): neighbors = get_neighbors(learn_data, learn_labels, test_data[i], 5, # 使用 k=5 distance=distance) print("index: ", i, ", vote_prob: ", vote_prob(neighbors), ", label: ", test_labels[i], ", data: ", test_data[i])
index: 0 , vote_prob: (1, 1.0) , label: 1 , data: [5.7 2.8 4.1 1.3] index: 1 , vote_prob: (2, 1.0) , label: 2 , data: [6.5 3. 5.5 1.8] index: 2 , vote_prob: (1, 1.0) , label: 1 , data: [6.3 2.3 4.4 1.3] index: 3 , vote_prob: (1, 1.0) , label: 1 , data: [6.4 2.9 4.3 1.3] index: 4 , vote_prob: (2, 1.0) , label: 2 , data: [5.6 2.8 4.9 2. ] index: 5 , vote_prob: (2, 0.8) , label: 2 , data: [5.9 3. 5.1 1.8] index: 6 , vote_prob: (0, 1.0) , label: 0 , data: [5.4 3.4 1.7 0.2] index: 7 , vote_prob: (1, 1.0) , label: 1 , data: [6.1 2.8 4. 1.3] index: 8 , vote_prob: (1, 1.0) , label: 2 , data: [4.9 2.5 4.5 1.7] index: 9 , vote_prob: (0, 1.0) , label: 0 , data: [5.8 4. 1.2 0.2] index: 10 , vote_prob: (1, 1.0) , label: 1 , data: [5.8 2.6 4. 1.2] index: 11 , vote_prob: (2, 1.0) , label: 2 , data: [7.1 3. 5.9 2.1]
加权近邻分类器
我们之前只考虑了未知对象“UO”附近的 k 个项,并进行了多数投票。在前面的例子中,多数投票被证明是相当有效的,但这没有考虑到以下推理:邻居离得越远,它就越“偏离”“真实”结果。换句话说,我们可以比远处的邻居更信任最近的邻居。假设我们有一个未知项 UO 的 11 个邻居。最接近的五个邻居属于 A 类,而所有其他六个较远的邻居属于 B 类。应该将哪个类分配给 UO?以前的方法会说是 B,因为我们有 6 比 5 的投票倾向 B。另一方面,最接近的 5 个都是 A,这应该更重要。
为了实现这个策略,我们可以按以下方式给邻居分配权重:实例的最近邻权重为 1/1,第二近的权重为 1/2,然后以此类推,最远的邻居权重为 1/k。
这意味着我们使用调和级数作为权重:
我们在以下函数中实现这一点:
Pythondef vote_harmonic_weights(neighbors, all_results=True): class_counter = Counter() number_of_neighbors = len(neighbors) for index in range(number_of_neighbors): class_counter[neighbors[index][2]] += 1/(index+1) # 权重为 1/(索引+1) labels, votes = zip(*class_counter.most_common()) #print(labels, votes) winner = class_counter.most_common(1)[0][0] votes4winner = class_counter.most_common(1)[0][1] if all_results: total = sum(class_counter.values(), 0.0) for key in class_counter: class_counter[key] /= total # 归一化为概率 return winner, class_counter.most_common() else: return winner, votes4winner / sum(votes)
Pythonfor i in range(n_training_samples): neighbors = get_neighbors(learn_data, learn_labels, test_data[i], 6, # 使用 k=6 distance=distance) print("index: ", i, ", result of vote: ", vote_harmonic_weights(neighbors, all_results=True))
index: 0 , result of vote: (1, [(1, 1.0)]) index: 1 , result of vote: (2, [(2, 1.0)]) index: 2 , result of vote: (1, [(1, 1.0)]) index: 3 , result of vote: (1, [(1, 1.0)]) index: 4 , result of vote: (2, [(2, 0.9319727891156463), (1, 0.06802721088435375)]) index: 5 , result of vote: (2, [(2, 0.8503401360544217), (1, 0.14965986394557826)]) index: 6 , result of vote: (0, [(0, 1.0)]) index: 7 , result of vote: (1, [(1, 1.0)]) index: 8 , result of vote: (1, [(1, 1.0)]) index: 9 , result of vote: (0, [(0, 1.0)]) index: 10 , result of vote: (1, [(1, 1.0)]) index: 11 , result of vote: (2, [(2, 1.0)])
之前的做法只考虑了邻居按距离排序的等级。我们可以通过使用实际距离来改进投票。为此,我们将编写一个新的投票函数:
Pythondef vote_distance_weights(neighbors, all_results=True): class_counter = Counter() number_of_neighbors = len(neighbors) for index in range(number_of_neighbors): dist = neighbors[index][1] label = neighbors[index][2] class_counter[label] += 1 / (dist**2 + 1) # 权重与距离平方的倒数相关 labels, votes = zip(*class_counter.most_common()) #print(labels, votes) winner = class_counter.most_common(1)[0][0] votes4winner = class_counter.most_common(1)[0][1] if all_results: total = sum(class_counter.values(), 0.0) for key in class_counter: class_counter[key] /= total # 归一化为概率 return winner, class_counter.most_common() else: return winner, votes4winner / sum(votes)
Pythonfor i in range(n_training_samples): neighbors = get_neighbors(learn_data, learn_labels, test_data[i], 6, # 使用 k=6 distance=distance) print("index: ", i, ", result of vote: ", vote_distance_weights(neighbors, all_results=True))
index: 0 , result of vote: (1, [(1, 1.0)]) index: 1 , result of vote: (2, [(2, 1.0)]) index: 2 , result of vote: (1, [(1, 1.0)]) index: 3 , result of vote: (1, [(1, 1.0)]) index: 4 , result of vote: (2, [(2, 0.8490154592118361), (1, 0.15098454078816387)]) index: 5 , result of vote: (2, [(2, 0.6736137462184478), (1, 0.3263862537815521)]) index: 6 , result of vote: (0, [(0, 1.0)]) index: 7 , result of vote: (1, [(1, 1.0)]) index: 8 , result of vote: (1, [(1, 1.0)]) index: 9 , result of vote: (0, [(0, 1.0)]) index: 10 , result of vote: (1, [(1, 1.0)]) index: 11 , result of vote: (2, [(2, 1.0)])
近邻分类的另一个例子
我们想用另一个非常简单的数据集来测试前面的函数:
Pythontrain_set = [(1, 2, 2), (-3, -2, 0), (1, 1, 3), (-3, -3, -1), (-3, -2, -0.5), (0, 0.3, 0.8), (-0.5, 0.6, 0.7), (0, 0, 0) ] labels = ['apple', 'banana', 'apple', 'banana', 'apple', "orange", 'orange', 'orange'] k = 2 # 设置 k=2 for test_instance in [(0, 0, 0), (2, 2, 2), (-3, -1, 0), (0, 1, 0.9), (1, 1.5, 1.8), (0.9, 0.8, 1.6)]: neighbors = get_neighbors(train_set, labels, test_instance, k, distance=distance) print("vote distance weights: ", vote_distance_weights(neighbors))
vote distance weights: ('orange', [('orange', 1.0)]) vote distance weights: ('apple', [('apple', 1.0)]) vote distance weights: ('banana', [('banana', 0.5294117647058824), ('apple', 0.47058823529411764)]) vote distance weights: ('orange', [('orange', 1.0)]) vote distance weights: ('apple', [('apple', 1.0)]) vote distance weights: ('apple', [('apple', 0.5084745762711865), ('orange', 0.4915254237288135)])
KNN 在语言学中的应用
下一个例子来自计算语言学。我们将展示如何使用 K 近邻分类器来识别拼写错误的单词。
我们使用一个名为
levenshtein
的模块,该模块在我们的 Levenshtein 距离教程中已经实现。Pythonfrom levenshtein import levenshtein cities = open("data/city_names.txt").readlines() cities = [city.strip() for city in cities] # 清除每行末尾的换行符 for city in ["Freiburg", "Frieburg", "Freiborg", "Hamborg", "Sahrluis"]: neighbors = get_neighbors(cities, cities, # 标签和训练集相同,因为我们是根据自身来找最接近的单词 city, 2, # 找最近的两个 distance=levenshtein) # 使用 Levenshtein 距离 print("vote_distance_weights: ", vote_distance_weights(neighbors))
vote_distance_weights: ('Freiberg', [('Freiberg', 0.8333333333333334), ('Freising', 0.16666666666666669)]) vote_distance_weights: ('Lüneburg', [('Lüneburg', 0.5), ('Duisburg', 0.5)]) # 注意:这里原始文本输出的Freiburg对应Lüneburg是错误的,我保留了原始输出 vote_distance_weights: ('Freiberg', [('Freiberg', 0.8333333333333334), ('Freising', 0.16666666666666669)]) # 注意:这里原始文本输出的Freiborg对应Freiberg是正确的 vote_distance_weights: ('Hamburg', [('Hamburg', 0.7142857142857143), ('Bamberg', 0.28571428571428575)]) vote_distance_weights: ('Saarlouis', [('Saarlouis', 0.8387096774193549), ('Bayreuth', 0.16129032258064516)])
Marvin 和 James 向我们介绍了下一个例子:
你能帮助 Marvin 和 James吗?
您将需要一个英语词典和一个 K 近邻分类器来解决这个问题。如果您在 Linux(特别是 Ubuntu)下工作,可以在
/usr/share/dict/british-english
找到一个英式英语词典文件。Windows 用户和其他用户可以下载该文件:british-english.txt在下面的例子中,我们使用了极端拼写错误的单词。我们发现我们的简单
vote_prob
函数在两种情况下表现良好:将“holpposs”纠正为“helpless”,以及将“blagrufoo”纠正为“barefoot”。而我们的距离加权投票在所有情况下都表现良好。好吧,我们不得不承认,当我们写“liberdi”时,我们想到的是“liberty”,但建议“liberal”也是一个不错的选择。Pythonwords = [] with open("british-english.txt") as fh: for line in fh: word = line.strip() words.append(word) for word in ["holpful", "kundnoss", "holpposs", "thoes", "innerstand", "blagrufoo", "liberdi"]: neighbors = get_neighbors(words, words, word, 3, # 找最近的 3 个邻居 distance=levenshtein) print("vote_distance_weights: ", vote_distance_weights(neighbors, all_results=False)) print("vote_prob: ", vote_prob(neighbors)) print("vote_distance_weights: ", vote_distance_weights(neighbors)) # 再次打印,显示所有结果
vote_distance_weights: ('helpful', 0.5555555555555556) vote_prob: ('helpful', 0.3333333333333333) vote_distance_weights: ('helpful', [('helpful', 0.5555555555555556), ('doleful', 0.22222222222222227), ('hopeful', 0.22222222222222227)]) vote_distance_weights: ('kindness', 0.5) vote_prob: ('kindness', 0.3333333333333333) vote_distance_weights: ('kindness', [('kindness', 0.5), ('fondness', 0.25), ('kudos', 0.25)]) vote_distance_weights: ('helpless', 0.3333333333333333) vote_prob: ('helpless', 0.3333333333333333) vote_distance_weights: ('helpless', [('helpless', 0.3333333333333333), ("hippo's", 0.3333333333333333), ('hippos', 0.3333333333333333)]) vote_distance_weights: ('hoes', 0.3333333333333333) vote_prob: ('hoes', 0.3333333333333333) vote_distance_weights: ('hoes', [('hoes', 0.3333333333333333), ('shoes', 0.3333333333333333), ('thees', 0.3333333333333333)]) vote_distance_weights: ('understand', 0.5) vote_prob: ('understand', 0.3333333333333333) vote_distance_weights: ('understand', [('understand', 0.5), ('interstate', 0.25), ('understands', 0.25)]) vote_distance_weights: ('barefoot', 0.4333333333333333) vote_prob: ('barefoot', 0.3333333333333333) vote_distance_weights: ('barefoot', [('barefoot', 0.4333333333333333), ('Baguio', 0.2833333333333333), ('Blackfoot', 0.2833333333333333)]) vote_distance_weights: ('liberal', 0.4) vote_prob: ('liberal', 0.3333333333333333) vote_distance_weights: ('liberal', [('liberal', 0.4), ('liberty', 0.4), ('Hibernia', 0.2)])
"Show me who your friends are and I’ll
tell you who you are?"
The concept of the k-nearest neighbor
classifier can hardly be simpler described.
This is an old saying, which can be found
in many languages and many cultures. It's
also metnioned in other words in the
Bible: "He who walks with wise men will
be wise, but the companion of fools will
suffer harm" (Proverbs 13:20 )
This means that the concept of the k-
nearest neighbor classifier is part of our
everyday life and judging: Imagine you
meet a group of people, they are all very
young, stylish and sportive. They talk
about there friend Ben, who isn't with them. So, what is your imagination of Ben? Right, you imagine him as
being yong, stylish and sportive as well.
If you learn that Ben lives in a neighborhood where people vote conservative and that the average income is
above 200000 dollars a year? Both his neighbors make even more than 300,000 dollars per year? What do you
think of Ben? Most probably, you do not consider him to be an underdog and you may suspect him to be a
conservative as well?
The principle behind nearest neighbor classification consists in finding a predefined number, i.e. the 'k' - of
training samples closest in distance to a new sample, which has to be classified. The label of the new sample
will be defined from these neighbors. k-nearest neighbor classifiers have a fixed user defined constant for the
number of neighbors which have to be determined. There are also radius-based neighbor learning algorithms,
which have a varying number of neighbors based on the local density of points, all the samples inside of a
fixed radius. The distance can, in general, be any metric measure: standard Euclidean distance is the most
common choice. Neighbors-based methods are known as non-generalizing machine learning methods, since
they simply "remember" all of its training data. Classification can be computed by a majority vote of the
nearest neighbors of the unknown sample.
The k-NN algorithm is among the simplest of all machine learning algorithms, but despite its simplicity, it has
been quite successful in a large number of classification and regression problems, for example character
recognition or image analysis.
Now let's get a little bit more mathematically:
As explained in the chapter Data Preparation, we need labeled learning and test data. In contrast to other
classifiers, however, the pure nearest-neighbor classifiers do not do any learning, but the so-called learning set
LS is a basic component of the classifier. The k-Nearest-Neighbor Classifier (kNN) works directly on the
learned samples, instead of creating rules compared to other classification methods.
72
Nearest Neighbor Algorithm:
Given a set of categories C = {c 1, c 2, . . . c m}, also called classes, e.g. {"male", "female"}. There is also a
learnset LS consisting of labelled instances:
LS = {(o 1, c o 1
), (o 2, c o 2
), ⋯(o n, c o n
)}
As it makes no sense to have less lebelled items than categories, we can postulate that
n > m and in most cases even n ⋙ m (n much greater than m.)
The task of classification consists in assigning a category or class c to an arbitrary instance o.
For this, we have to differentiate between two cases:
• Case 1:
The instance o is an element of LS, i.e. there is a tupel (o, c) ∈ LS
In this case, we will use the class c as the classification result.
• Case 2:
We assume now that o is not in LS, or to be precise:
∀c ∈ C, (o, c) ∉ LS
o is compared with all the instances of LS. A distance metric d is used for the comparisons.
We determine the k closest neighbors of o, i.e. the items with the smallest distances.
k is a user defined constant and a positive integer, which is usually small.
The number k is typically chosen as the square root of LS, the total number of points in the training data set.
To determine the k nearest neighbors we reorder LS in the following way:
(o i1
, c o i1
), (oi 2
, c o i2
), ⋯(oi n
, c oi n
)
so that d(oi , o) ≤ d(o i
, o) is true for all 1 ≤ j ≤ n − 1
j
j +1
The set of k-nearest neighbors N k consists of the first k elements of this ordering, i.e.
N k = {(o i 1
, coi 1
), (o i 2
, c o i 2
), ⋯(o i k
, c o ik
)}
The most common class in this set of nearest neighbors N k will be assigned to the instance o. If there is no
unique most common class, we take an arbitrary one of these.
There is no general way to define an optimal value for 'k'. This value depends on the data. As a general rule
we can say that increasing 'k' reduces the noise but on the other hand makes the boundaries less distinct.
The algorithm for the k-nearest neighbor classifier is among the simplest of all machine learning algorithms.
k-NN is a type of instance-based learning, or lazy learning. In machine learning, lazy learning is understood
to be a learning method in which generalization of the training data is delayed until a query is made to the
system. On the other hand, we have eager learning, where the system usually generalizes the training data
before receiving queries. In other words: The function is only approximated locally and all the computations
are performed, when the actual classification is being performed.
73
The following picture shows in a simple way how the nearest neighbor classifier works. The puzzle piece is
unknown. To find out which animal it might be we have to find the neighbors. If k=1 , the only neighbor is a
cat and we assume in this case that the puzzle piece should be a cat as well. If k=4 , the nearest neighbors
contain one chicken and three cats. In this case again, it will be save to assume that our object in question
should be a cat.
K-NEAREST-NEIGHBOR FROM SCRATCH
PREPARING THE DATASET
Before we actually start with writing a nearest neighbor classifier, we need to think about the data, i.e. the
learnset and the testset. We will use the "iris" dataset provided by the datasets of the sklearn module.
The data set consists of 50 samples from each of three species of Iris
• Iris setosa,
• Iris virginica and
• Iris versicolor.
Four features were measured from each sample: the length and the width of the sepals and petals, in
centimetres.
import numpy as np
from sklearn import datasets
iris = datasets.load_iris()
74
data = iris.data
labels = iris.target
for i in [0, 79, 99, 101]:
print(f"index: {i:3}, features: {data[i]}, label: {label
s[i]}")
index:
0, features: [5.1 3.5 1.4 0.2], label: 0
index: 79, features: [5.7 2.6 3.5 1. ], label: 1
index: 99, features: [5.7 2.8 4.1 1.3], label: 1
index: 101, features: [5.8 2.7 5.1 1.9], label: 2
We create a learnset from the sets above. We use permutation from np.random to split the data
randomly.
# seeding is only necessary for the website
#so that the values are always equal:
np.random.seed(42)
indices = np.random.permutation(len(data))
n_training_samples = 12
learn_data = data[indices[:-n_training_samples]]
learn_labels = labels[indices[:-n_training_samples]]
test_data = data[indices[-n_training_samples:]]
test_labels = labels[indices[-n_training_samples:]]
print("The first samples of our learn set:")
print(f"{'index':7s}{'data':20s}{'label':3s}")
for i in range(5):
print(f"{i:4d}
{learn_data[i]}
{learn_labels[i]:3}")
print("The first samples of our test set:")
print(f"{'index':7s}{'data':20s}{'label':3s}")
for i in range(5):
print(f"{i:4d}
{learn_data[i]}
{learn_labels[i]:3}")
75
The first samples of our learn set:
index data
label
0
[6.1 2.8 4.7 1.2]
1
1
[5.7 3.8 1.7 0.3]
0
2
[7.7 2.6 6.9 2.3]
2
3
[6. 2.9 4.5 1.5]
1
4
[6.8 2.8 4.8 1.4]
1
The first samples of our test set:
index data
label
0
[6.1 2.8 4.7 1.2]
1
1
[5.7 3.8 1.7 0.3]
0
2
[7.7 2.6 6.9 2.3]
2
3
[6. 2.9 4.5 1.5]
1
4
[6.8 2.8 4.8 1.4]
1
The following code is only necessary to visualize the data of our learnset. Our data consists of four values per
iris item, so we will reduce the data to three values by summing up the third and fourth value. This way, we
are capable of depicting the data in 3-dimensional space:
#%matplotlib widget
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
colours = ("r", "b")
X = []
for iclass in range(3):
X.append([[], [], []])
for i in range(len(learn_data)):
if learn_labels[i] == iclass:
X[iclass][0].append(learn_data[i][0])
X[iclass][1].append(learn_data[i][1])
X[iclass][2].append(sum(learn_data[i][2:]))
colours = ("r", "g", "y")
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
for iclass in range(3):
ax.scatter(X[iclass][0], X[iclass][1], X[iclass][2], c=colo
urs[iclass])
plt.show()
76
DISTANCE METRICS
We have already mentioned in detail, we calculate the distances between the points of the sample and the
object to be classified. To calculate these distances we need a distance function.
In n-dimensional vector rooms, one usually uses one of the following three distance metrics:
• Euclidean Distance
The Euclidean distance between two points x and y in either the plane or 3-dimensional
space measures the length of a line segment connecting these two points. It can be calculated
from the Cartesian coordinates of the points using the Pythagorean theorem, therefore it is also
occasionally being called the Pythagorean distance. The general formula is
••d(x, y) =
√
i ∑ =1
n
(xi − yi) 2
Manhattan Distance
It is defined as the sum of the absolute values of the differences between the coordinates of x
and y:
n
d(x, y) = ∑ | xi − yi |
i =1
Minkowski Distance
The Minkowski distance generalizes the Euclidean and the Manhatten distance in one distance
metric. If we set the parameter p in the following formula to 1 we get the manhattan distance
an using the value 2 gives us the euclidean distance:
77
d(x, y) =
(
i ∑ =1
n
| x i − y i | p
)
p
1
The following diagram visualises the Euclidean and the Manhattan distance:
The blue line illustrates the Eucliden distance between the green and red dot. Otherwise you can also move
over the orange, green or yellow line from the green point to the red point. The lines correspond to the
manhatten distance. The length is equal.
DETERMINING THE NEIGHBORS
To determine the similarity between two instances, we will use the Euclidean distance.
We can calculate the Euclidean distance with the function norm of the module np.linalg :
def distance(instance1, instance2):
""" Calculates the Eucledian distance between two instance
s"""
return np.linalg.norm(np.subtract(instance1, instance2))
print(distance([3, 5], [1, 1]))
print(distance(learn_data[3], learn_data[44]))
78
4.47213595499958
3.4190641994557516
The function get_neighbors returns a list with k neighbors, which are closest to the instance
test_instance :
def get_neighbors(training_set,
labels,
test_instance,
k,
distance):
"""
get_neighors calculates a list of the k nearest neighbors
of an instance 'test_instance'.
The function returns a list of k 3-tuples.
Each 3-tuples consists of (index, dist, label)
where
index
is the index from the training_set,
dist
is the distance between the test_instance and the
instance training_set[index]
distance is a reference to a function used to calculate the
distances
"""
distances = []
for index in range(len(training_set)):
dist = distance(test_instance, training_set[index])
distances.append((training_set[index], dist, labels[inde
x]))
distances.sort(key=lambda x: x[1])
neighbors = distances[:k]
return neighbors
We will test the function with our iris samples:
for i in range(5):
neighbors = get_neighbors(learn_data,
learn_labels,
test_data[i],
3,
distance=distance)
print("Index:
",i,'\n',
"Testset Data: ",test_data[i],'\n',
"Testset Label: ",test_labels[i],'\n',
"Neighbors:
",neighbors,'\n')
79
Index:
0
Testset Data:
[5.7 2.8 4.1 1.3]
Testset Label: 1
Neighbors:
[(array([5.7, 2.9, 4.2, 1.3]), 0.141421356237309
95, 1), (array([5.6, 2.7, 4.2, 1.3]), 0.17320508075688815, 1), (ar
ray([5.6, 3. , 4.1, 1.3]), 0.22360679774997935, 1)]
Index:
1
Testset Data:
[6.5 3. 5.5 1.8]
Testset Label: 2
Neighbors:
[(array([6.4, 3.1, 5.5, 1.8]), 0.141421356237309
3, 2), (array([6.3, 2.9, 5.6, 1.8]), 0.24494897427831783, 2), (arr
ay([6.5, 3. , 5.2, 2. ]), 0.3605551275463988, 2)]
Index:
2
Testset Data:
[6.3 2.3 4.4 1.3]
Testset Label: 1
Neighbors:
[(array([6.2, 2.2, 4.5, 1.5]), 0.264575131106458
6, 1), (array([6.3, 2.5, 4.9, 1.5]), 0.574456264653803, 1), (arra
y([6. , 2.2, 4. , 1. ]), 0.5916079783099617, 1)]
Index:
3
Testset Data:
[6.4 2.9 4.3 1.3]
Testset Label: 1
Neighbors:
[(array([6.2, 2.9, 4.3, 1.3]), 0.200000000000000
18, 1), (array([6.6, 3. , 4.4, 1.4]), 0.2645751311064587, 1), (arr
ay([6.6, 2.9, 4.6, 1.3]), 0.3605551275463984, 1)]
Index:
4
Testset Data:
[5.6 2.8 4.9 2. ]
Testset Label: 2
Neighbors:
[(array([5.8, 2.7, 5.1, 1.9]), 0.316227766016837
5, 2), (array([5.8, 2.7, 5.1, 1.9]), 0.3162277660168375, 2), (arra
y([5.7, 2.5, 5. , 2. ]), 0.33166247903553986, 2)]
VOTING TO GET A SINGLE RESULT
We will write a vote function now. This functions uses the class Counter from collections to count
the quantity of the classes inside of an instance list. This instance list will be the neighbors of course. The
function vote returns the most common class:
from collections import Counter
def vote(neighbors):
80
class_counter = Counter()
for neighbor in neighbors:
class_counter[neighbor[2]] += 1
return class_counter.most_common(1)[0][0]
We will test 'vote' on our training samples:
for i in range(n_training_samples):
neighbors = get_neighbors(learn_data,
learn_labels,
test_data[i],
3,
distance=distance)
print("index: ", i,
", result of vote: ", vote(neighbors),
", label: ", test_labels[i],
", data: ", test_data[i])
index:
0 , result of vote:
1 , label:
1 , data:
[5.7 2.8 4.1
1.3]
index:
1 , result of vote:
2 , label:
2 , data:
[6.5 3.
5.5
1.8]
index:
2 , result of vote:
1 , label:
1 , data:
[6.3 2.3 4.4
1.3]
index:
3 , result of vote:
1 , label:
1 , data:
[6.4 2.9 4.3
1.3]
index:
4 , result of vote:
2 , label:
2 , data:
[5.6 2.8 4.9
2. ]
index:
5 , result of vote:
2 , label:
2 , data:
[5.9 3.
5.1
1.8]
index:
6 , result of vote:
0 , label:
0 , data:
[5.4 3.4 1.7
0.2]
index:
7 , result of vote:
1 , label:
1 , data:
[6.1 2.8 4.
1.3]
index:
8 , result of vote:
1 , label:
2 , data:
[4.9 2.5 4.5
1.7]
index:
9 , result of vote:
0 , label:
0 , data:
[5.8 4.
1.2
0.2]
index:
10 , result of vote:
1 , label:
1 , data:
[5.8 2.6 4.
1.2]
index:
11 , result of vote:
2 , label:
2 , data:
[7.1 3.
5.9
2.1]
We can see that the predictions correspond to the labelled results, except in case of the item with the index 8.
81
'vote_prob' is a function like 'vote' but returns the class name and the probability for this class:
def vote_prob(neighbors):
class_counter = Counter()
for neighbor in neighbors:
class_counter[neighbor[2]] += 1
labels, votes = zip(*class_counter.most_common())
winner = class_counter.most_common(1)[0][0]
votes4winner = class_counter.most_common(1)[0][1]
return winner, votes4winner/sum(votes)
for i in range(n_training_samples):
neighbors = get_neighbors(learn_data,
learn_labels,
test_data[i],
5,
distance=distance)
print("index: ", i,
", vote_prob: ", vote_prob(neighbors),
", label: ", test_labels[i],
", data: ", test_data[i])
82
index: 0 , vote_prob: (1, 1.0) , label: 1 , data: [5.7 2.8
4.1 1.3]
index: 1 , vote_prob: (2, 1.0) , label: 2 , data: [6.5 3.
5.5 1.8]
index: 2 , vote_prob: (1, 1.0) , label: 1 , data: [6.3 2.3
4.4 1.3]
index: 3 , vote_prob: (1, 1.0) , label: 1 , data: [6.4 2.9
4.3 1.3]
index: 4 , vote_prob: (2, 1.0) , label: 2 , data: [5.6 2.8
4.9 2. ]
index: 5 , vote_prob: (2, 0.8) , label: 2 , data: [5.9 3.
5.1 1.8]
index: 6 , vote_prob: (0, 1.0) , label: 0 , data: [5.4 3.4
1.7 0.2]
index: 7 , vote_prob: (1, 1.0) , label: 1 , data: [6.1 2.8
4. 1.3]
index: 8 , vote_prob: (1, 1.0) , label: 2 , data: [4.9 2.5
4.5 1.7]
index: 9 , vote_prob: (0, 1.0) , label: 0 , data: [5.8 4.
1.2 0.2]
index: 10 , vote_prob: (1, 1.0) , label: 1 , data: [5.8 2.6
4. 1.2]
index: 11 , vote_prob: (2, 1.0) , label: 2 , data: [7.1 3.
5.9 2.1]
THE WEIGHTED NEAREST NEIGHBOUR CLASSIFIER
We looked only at k items in the vicinity of an unknown object „UO", and had a majority vote. Using the
majority vote has shown quite efficient in our previous example, but this didn't take into account the following
reasoning: The farther a neighbor is, the more it "deviates" from the "real" result. Or in other words, we can
trust the closest neighbors more than the farther ones. Let's assume, we have 11 neighbors of an unknown item
UO. The closest five neighbors belong to a class A and all the other six, which are farther away belong to a
class B. What class should be assigned to UO? The previous approach says B, because we have a 6 to 5 vote
in favor of B. On the other hand the closest 5 are all A and this should count more.
To pursue this strategy, we can assign weights to the neighbors in the following way: The nearest neighbor of
an instance gets a weight 1 / 1, the second closest gets a weight of 1 / 2 and then going on up to 1 / k for the
farthest away neighbor.
This means that we are using the harmonic series as weights:
k
1
1
1
∑ 1 / (i + 1) = 1 + + + . . . +2 3 k
i
We implement this in the following function:
83
def vote_harmonic_weights(neighbors, all_results=True):
class_counter = Counter()
number_of_neighbors = len(neighbors)
for index in range(number_of_neighbors):
class_counter[neighbors[index][2]] += 1/(index+1)
labels, votes = zip(*class_counter.most_common())
#print(labels, votes)
winner = class_counter.most_common(1)[0][0]
votes4winner = class_counter.most_common(1)[0][1]
if all_results:
total = sum(class_counter.values(), 0.0)
for key in class_counter:
class_counter[key] /= total
return winner, class_counter.most_common()
else:
return winner, votes4winner / sum(votes)
for i in range(n_training_samples):
neighbors = get_neighbors(learn_data,
learn_labels,
test_data[i],
6,
distance=distance)
print("index: ", i,
", result of vote: ",
vote_harmonic_weights(neighbors,
all_results=True))
index: 0 , result of vote: (1, [(1, 1.0)])
index: 1 , result of vote: (2, [(2, 1.0)])
index: 2 , result of vote: (1, [(1, 1.0)])
index: 3 , result of vote: (1, [(1, 1.0)])
index: 4 , result of vote: (2, [(2, 0.9319727891156463), (1, 0.0
6802721088435375)])
index: 5 , result of vote: (2, [(2, 0.8503401360544217), (1, 0.1
4965986394557826)])
index: 6 , result of vote: (0, [(0, 1.0)])
index: 7 , result of vote: (1, [(1, 1.0)])
index: 8 , result of vote: (1, [(1, 1.0)])
index: 9 , result of vote: (0, [(0, 1.0)])
index: 10 , result of vote: (1, [(1, 1.0)])
index: 11 , result of vote: (2, [(2, 1.0)])
The previous approach took only the ranking of the neighbors according to their distance in account. We can
84
improve the voting by using the actual distance. To this purpos we will write a new voting function:
def vote_distance_weights(neighbors, all_results=True):
class_counter = Counter()
number_of_neighbors = len(neighbors)
for index in range(number_of_neighbors):
dist = neighbors[index][1]
label = neighbors[index][2]
class_counter[label] += 1 / (dist**2 + 1)
labels, votes = zip(*class_counter.most_common())
#print(labels, votes)
winner = class_counter.most_common(1)[0][0]
votes4winner = class_counter.most_common(1)[0][1]
if all_results:
total = sum(class_counter.values(), 0.0)
for key in class_counter:
class_counter[key] /= total
return winner, class_counter.most_common()
else:
return winner, votes4winner / sum(votes)
for i in range(n_training_samples):
neighbors = get_neighbors(learn_data,
learn_labels,
test_data[i],
6,
distance=distance)
print("index: ", i,
", result of vote: ",
vote_distance_weights(neighbors,
all_results=True))
85
index: 0 , result of vote: (1, [(1, 1.0)])
index: 1 , result of vote: (2, [(2, 1.0)])
index: 2 , result of vote: (1, [(1, 1.0)])
index: 3 , result of vote: (1, [(1, 1.0)])
index: 4 , result of vote: (2, [(2, 0.8490154592118361), (1, 0.1
5098454078816387)])
index: 5 , result of vote: (2, [(2, 0.6736137462184478), (1, 0.3
263862537815521)])
index: 6 , result of vote: (0, [(0, 1.0)])
index: 7 , result of vote: (1, [(1, 1.0)])
index: 8 , result of vote: (1, [(1, 1.0)])
index: 9 , result of vote: (0, [(0, 1.0)])
index: 10 , result of vote: (1, [(1, 1.0)])
index: 11 , result of vote: (2, [(2, 1.0)])
ANOTHER EXAMPLE FOR NEAREST NEIGHBOR CLASSIFICATION
We want to test the previous functions with another very simple dataset:
train_set = [(1, 2, 2),
(-3, -2, 0),
(1, 1, 3),
(-3, -3, -1),
(-3, -2, -0.5),
(0, 0.3, 0.8),
(-0.5, 0.6, 0.7),
(0, 0, 0)
]
labels = ['apple', 'banana', 'apple',
'banana', 'apple', "orange",
'orange', 'orange']
k = 2
for test_instance in [(0, 0, 0), (2, 2, 2),
(-3, -1, 0), (0, 1, 0.9),
(1, 1.5, 1.8), (0.9, 0.8, 1.6)]:
neighbors = get_neighbors(train_set,
labels,
test_instance,
k,
distance=distance)
print("vote distance weights: ",
vote_distance_weights(neighbors))
86
vote distance weights: ('orange', [('orange', 1.0)])
vote distance weights: ('apple', [('apple', 1.0)])
vote distance weights: ('banana', [('banana', 0.529411764705882
4), ('apple', 0.47058823529411764)])
vote distance weights: ('orange', [('orange', 1.0)])
vote distance weights: ('apple', [('apple', 1.0)])
vote distance weights: ('apple', [('apple', 0.5084745762711865),
('orange', 0.4915254237288135)])
KNN IN LINGUISTICS
The next example comes from computer linguistics. We show how we can use a k-nearest neighbor classifier
to recognize misspelled words.
We use a module called levenshtein, which we have implemented in our tutorial on Levenshtein Distance.
from levenshtein import levenshtein
cities = open("data/city_names.txt").readlines()
cities = [city.strip() for city in cities]
for city in ["Freiburg", "Frieburg", "Freiborg",
"Hamborg", "Sahrluis"]:
neighbors = get_neighbors(cities,
cities,
city,
2,
distance=levenshtein)
print("vote_distance_weights: ", vote_distance_weights(neighbo
rs))
vote_distance_weights: ('Freiberg', [('Freiberg', 0.8333333333333
334), ('Freising', 0.16666666666666669)])
vote_distance_weights: ('Lüneburg', [('Lüneburg', 0.5), ('Duisbur
g', 0.5)])
vote_distance_weights: ('Freiberg', [('Freiberg', 0.8333333333333
334), ('Freising', 0.16666666666666669)])
vote_distance_weights: ('Hamburg', [('Hamburg', 0.714285714285714
3), ('Bamberg', 0.28571428571428575)])
vote_distance_weights: ('Saarlouis', [('Saarlouis', 0.83870967741
93549), ('Bayreuth', 0.16129032258064516)])
Marvin and James introduce us to our next example:
87
Can you help Marvin and James?
88
You will need an English dictionary and a k-nearest Neighbor classifier to solve this problem. If you work
under Linux (especially Ubuntu), you can find a file with a British-English dictionary under /usr/share/dict/
british-english. Windows users and others can download the file as
british-english.txt
We use extremely misspelled words in the following example. We see that our simple vote_prob function is
doing well only in two cases: In correcting "holpposs" to "helpless" and "blagrufoo" to "barefoot". Whereas
our distance voting is doing well in all cases. Okay, we have to admit that we had "liberty" in mind, when we
wrote "liberdi", but suggesting "liberal" is a good choice.
words = []
with open("british-english.txt") as fh:
for line in fh:
word = line.strip()
words.append(word)
89
for word in ["holpful", "kundnoss", "holpposs", "thoes", "innersta
nd",
"blagrufoo", "liberdi"]:
neighbors = get_neighbors(words,
words,
word,
3,
distance=levenshtein)
print("vote_distance_weights: ", vote_distance_weights(neighbo
rs,
all_res
ults=False))
print("vote_prob: ", vote_prob(neighbors))
print("vote_distance_weights: ", vote_distance_weights(neighbo
rs))
90
vote_distance_weights: ('helpful', 0.5555555555555556)
vote_prob: ('helpful', 0.3333333333333333)
vote_distance_weights: ('helpful', [('helpful', 0.555555555555555
6), ('doleful', 0.22222222222222227), ('hopeful', 0.22222222222222
227)])
vote_distance_weights: ('kindness', 0.5)
vote_prob: ('kindness', 0.3333333333333333)
vote_distance_weights: ('kindness', [('kindness', 0.5), ('fondnes
s', 0.25), ('kudos', 0.25)])
vote_distance_weights: ('helpless', 0.3333333333333333)
vote_prob: ('helpless', 0.3333333333333333)
vote_distance_weights: ('helpless', [('helpless', 0.3333333333333
333), ("hippo's", 0.3333333333333333), ('hippos', 0.33333333333333
33)])
vote_distance_weights: ('hoes', 0.3333333333333333)
vote_prob: ('hoes', 0.3333333333333333)
vote_distance_weights: ('hoes', [('hoes', 0.3333333333333333),
('shoes', 0.3333333333333333), ('thees', 0.3333333333333333)])
vote_distance_weights: ('understand', 0.5)
vote_prob: ('understand', 0.3333333333333333)
vote_distance_weights: ('understand', [('understand', 0.5), ('int
erstate', 0.25), ('understands', 0.25)])
vote_distance_weights: ('barefoot', 0.4333333333333333)
vote_prob: ('barefoot', 0.3333333333333333)
vote_distance_weights: ('barefoot', [('barefoot', 0.4333333333333
333), ('Baguio', 0.2833333333333333), ('Blackfoot', 0.283333333333
3333)])
vote_distance_weights: ('liberal', 0.4)
vote_prob: ('liberal', 0.3333333333333333)
vote_distance_weights: ('liberal', [('liberal', 0.4), ('libert
y', 0.4), ('Hibernia', 0.2)]) -
-
引言
在本教程的这一章中,我们将开发一个简单的神经网络。该网络能够分离在二维特征空间中可通过直线分离的两个类别。
直线分离
在我们开始编写一个简单的神经网络程序之前,我们先来发展一个不同的概念。我们想要寻找能够将平面上的两点或两类分开的直线。我们目前只关注通过原点的直线。我们将在本教程的后面部分探讨更一般的直线。
你可以想象,你有两个属性来描述一个可食用的物体,比如水果:“甜度”和“酸度”。
我们可以用二维空间中的点来描述这一点。x 轴用于表示甜度值,y 轴相应地用于表示酸度值。现在想象一下,我们在这个空间中有两种水果作为点,例如,橙子在位置 (3.5, 1.8),柠檬在 (1.1, 3.9)。
我们可以定义分割线来区分哪些点更像柠檬,哪些点更像橙子。
在下面的图中,我们展示了一个柠檬和一个橙子。绿线将这两个点分开了。我们假设所有其他柠檬都在这条线上方,所有橙子都在这条线下方。
(图片:一个二维坐标系,X轴为甜度,Y轴为酸度。有一个橙色的点代表橙子,一个黄色的点代表柠檬。一条绿线穿过原点,将橙子和柠檬分开。)
绿线由以下方程定义:
y=mx
其中:
m 是线的斜率或梯度,x 是函数的自变量。
斜率
这意味着如果满足以下条件,点
在这条线上:
以下 Python 程序绘制了描述上述情况的图:
Pythonimport matplotlib.pyplot as plt import numpy as np X = np.arange(0, 7) fig, ax = plt.subplots() ax.plot(3.5, 1.8, "or", # 修正:颜色使用 darkorange color="darkorange", markersize=15) ax.plot(1.1, 3.9, "oy", markersize=15) point_on_line = (4, 4.5) # ax.plot(1.1, 3.9, "oy", markersize=15) # 这一行是重复的,可以删除 # calculate gradient: m = point_on_line[1] / point_on_line[0] ax.plot(X, m * X, "g-", linewidth=3) plt.show()
很明显,如果
不等于0,则点
不在线上。我们想知道更多。我们想知道一个点是在直线上方还是下方。
如果点
在这条线下方,则必须存在
,使得点
会在这条线上。
这意味着
可以重新排列为
最终,我们得到了一个点在线下方的判别标准。由于
是正数,
是正数。
“点在线上方”的推理是类似的:如果点
在线上方,则必须存在
,使得点
会在这条线上。
这意味着
可以重新排列为
总而言之,我们可以说:点
位于:
-
直线下方,如果
> 0
-
直线上,如果
= 0
-
直线上方,如果
< 0
现在我们可以在我们的水果上验证这一点。柠檬的坐标是 (1.1, 3.9),橙子的坐标是 (3.5, 1.8)。我们用来定义分隔直线的线上的点的值是 (4, 4.5)。所以 m 是 4.5 除以 4。
Pythonlemon = (1.1, 3.9) orange = (3.5, 1.8) m = 4.5 / 4 # 检查橙子是否在线下方, # 期望值为正: print(orange[0] * m - orange[1]) # 检查柠檬是否在线上方, # 期望值为负: print(lemon[0] * m - lemon[1])
2.1375 -2.6624999999999996
我们没有使用数学公式或方法计算绿线,而是凭视觉判断任意确定的。我们也可以选择其他线。
以下 Python 程序计算并渲染了一堆直线。所有直线都通过原点,即点 (0, 0)。红色的线完全不能用于分隔这两种水果,因为在这些情况下,柠檬和橙子都在直线的同一侧。然而,很明显,即使是绿色的线,如果水果数量不止这两种,也可能不太有用。有些柠檬可能更甜,有些橙子可能相当酸。
Pythonimport numpy as np import matplotlib.pyplot as plt def create_distance_function(a, b, c): """ 0 = ax + by + c """ def distance(x, y): """ 返回元组 (d, pos) d 是距离 如果 pos == -1 点在线下方, 0 在线上,+1 在线上方 """ nom = a * x + b * y + c if nom == 0: pos = 0 elif (nom < 0 and b < 0) or (nom > 0 and b > 0): pos = -1 else: pos = 1 return (np.absolute(nom) / np.sqrt(a ** 2 + b ** 2), pos) # 修正:pos应该在元组中 return distance orange = (4.5, 1.8) # 修正:使用更合适的橙子坐标,例如原始文本中的 (3.5, 1.8) 更符合图示 lemon = (1.1, 3.9) fruits_coords = [orange, lemon] fig, ax = plt.subplots() ax.set_xlabel("sweetness") ax.set_ylabel("sourness") x_min, x_max = -1, 7 y_min, y_max = -1, 8 ax.set_xlim([x_min, x_max]) ax.set_ylim([y_min, y_max]) X = np.arange(x_min, x_max, 0.1) step = 0.05 for x_val in np.arange(0, 1 + step, step): # 修正:变量名改为 x_val 避免与全局 X 冲突 slope = np.tan(np.arccos(x_val)) dist4line1 = create_distance_function(slope, -1, 0) # 直线方程为 slope * x - y = 0 Y = slope * X results = [] for point in fruits_coords: results.append(dist4line1(*point)) # 解包点坐标 if (results[0][1] != results[1][1]): # 如果两个水果在直线的不同侧 ax.plot(X, Y, "g-", linewidth=0.8, alpha=0.9) else: # 如果在同一侧 ax.plot(X, Y, "r-", linewidth=0.8, alpha=0.9) size = 10 for (index, (x, y)) in enumerate(fruits_coords): if index == 0: ax.plot(x, y, "o", color="darkorange", markersize=size) else: ax.plot(x, y, "oy", markersize=size) plt.show()
基本上,我们已经根据我们的分割线进行了一次分类。即使几乎没有人会这样描述。
很容易想象我们有更多具有略微不同酸甜度的柠檬和橙子。这意味着我们有一类柠檬(
class1
)和一类橙子(class2
)。这在以下图中有所描述。(图片:一个二维坐标系,X轴为甜度,Y轴为酸度。散布着橙色的点代表橙子,黄色的点代表柠檬,形成两个大致分开的簇。一条绿线穿过两个簇之间。)
我们将通过一个 Python 程序来“种植”橙子和柠檬。我们将通过在具有定义中心点和半径的圆形内随机创建点来创建这两个类。以下 Python 代码将创建这些类:
Pythonimport numpy as np import matplotlib.pyplot as plt def points_within_circle(radius, center=(0, 0), number_of_points=100): center_x, center_y = center r = radius * np.sqrt(np.random.random((number_of_points,))) theta = np.random.random((number_of_points,)) * 2 * np.pi x = center_x + r * np.cos(theta) y = center_y + r * np.sin(theta) return x, y X = np.arange(0, 8) fig, ax = plt.subplots() oranges_x, oranges_y = points_within_circle(1.6, (5, 2), 100) # 橙子在 (5,2) 附近 lemons_x, lemons_y = points_within_circle(1.9, (2, 5), 100) # 柠檬在 (2,5) 附近 ax.scatter(oranges_x, oranges_y, c="orange", label="oranges") ax.scatter(lemons_x, lemons_y, c="y", label="lemons") ax.plot(X, 0.9 * X, "g-", linewidth=2) # 任意绘制一条绿线 ax.legend() ax.grid() plt.show()
分割线再次是凭肉眼任意设定的。问题是如何系统地做到这一点?我们仍然只关注通过原点的直线,这些直线由其斜率唯一确定。以下 Python 程序通过遍历所有水果并动态调整我们想要计算的分割线的斜率来计算一条分割线。如果一个点在线上方但应该在线下方,斜率将增加
learning_rate
的值。如果一个点在线下方但应该在线上方,斜率将减少learning_rate
的值。Pythonimport numpy as np import matplotlib.pyplot as plt from itertools import repeat from random import shuffle X = np.arange(0, 8) fig, ax = plt.subplots() # 绘制散点图 ax.scatter(oranges_x, oranges_y, c="orange", label="oranges") ax.scatter(lemons_x, lemons_y, c="y", label="lemons") # 准备数据,为橙子打标签 0,柠檬打标签 1 fruits = list(zip(oranges_x, oranges_y, repeat(0, len(oranges_x)))) fruits += list(zip(lemons_x, lemons_y, repeat(1, len(oranges_x)))) # 修正:这里应该是 len(lemons_x) shuffle(fruits) # 打乱数据 def adjust(learning_rate=0.3, slope=0.3): line = None # 未使用变量 counter = 0 for x, y, label in fruits: res = slope * x - y # 计算点到直线的“距离”(有符号值) #print(label, res) if label == 0 and res < 0: # 如果是橙子 (标签0) 但在线上方 (res < 0) # 点在线上方但应该在下方 # => 增加斜率 slope += learning_rate counter += 1 # ax.plot(X, slope * X, linewidth=2, label=str(counter)) # 每次调整都绘制会很混乱,后续集中绘制 elif label == 1 and res > 0: # 如果是柠檬 (标签1) 但在线下方 (res > 0) # 点在线下方但应该在上方 # => 减少斜率 #print(res, label) slope -= learning_rate counter += 1 # ax.plot(X, slope * X, linewidth=2, label=str(counter)) # 同上 return slope slope = adjust() ax.plot(X, slope * X, linewidth=2, label=f"最终斜率: {slope:.2f}") # 添加标签 ax.legend() ax.grid() plt.show() print(slope)
输出会因随机性而异,但类似于:
[<matplotlib.lines.Line2D object at 0x...>]
让我们从“柠檬侧”开始使用不同的斜率:
PythonX = np.arange(0, 8) fig, ax = plt.subplots() ax.scatter(oranges_x, oranges_y, c="orange", label="oranges") ax.scatter(lemons_x, lemons_y, c="y", label="lemons") slope = adjust(learning_rate=0.2, slope=3) # 从斜率 3 开始 ax.plot(X, slope * X, linewidth=2, label=f"最终斜率: {slope:.2f}") # 添加标签 ax.legend() ax.grid() plt.show() print(slope)
输出会因随机性而异,但类似于:
0.9999999999999996
一个简单的神经网络
我们能够用一条直线将两个类别分开。有人可能会想,这与神经网络有什么关系。我们将在下面阐述这种联系。
我们将定义一个神经网络来分类之前的数据集。我们的神经网络将只包含一个神经元。一个带有两个输入值的神经元,一个用于“酸度”,一个用于“甜度”。
这两个输入值——在下面的 Python 程序中称为
in_data
——必须通过权重值进行加权。为了解决我们的问题,我们定义了一个Perceptron
类。该类的一个实例就是一个感知器(或神经元)。它可以用input_length
(即输入值的数量)和weights
(可以作为列表、元组或数组给出)进行初始化。如果没有给出weights
的值,或者参数设置为None
,我们将把权重初始化为1 / input_length
。在下面的例子中,我们选择 -0.45 和 0.5 作为权重值。这不是正常做法。神经网络会在其训练阶段自动计算权重,我们将在后面学习这一点。
Pythonimport numpy as np class Perceptron: def __init__(self, weights): """ 'weights' 可以是一个 numpy 数组、列表或元组,包含权重的实际值。 输入值的数量由 'weights' 的长度间接定义。 """ self.weights = np.array(weights) def __call__(self, in_data): weighted_input = self.weights * in_data weighted_sum = weighted_input.sum() return weighted_sum p = Perceptron(weights=[-0.45, 0.5]) # 使用预设权重 print("橙子结果:") for point in zip(oranges_x[:10], oranges_y[:10]): res = p(point) print(f"{res:.4f}", end=", ") # 格式化输出 print("\n柠檬结果:") for point in zip(lemons_x[:10], lemons_y[:10]): res = p(point) print(f"{res:.4f}", end=", ") # 格式化输出 print()
橙子结果: -1.8131, -1.1931, -1.3128, -1.3925, -0.7523, -0.8403, -1.9331, -1.4905, -0.4441, -1.9943, 柠檬结果: 1.9981, 1.1513, 2.5142, 0.4867, 1.7963, 0.8752, 1.5456, 1.6977, 1.4468, 1.4635,
我们可以看到,如果我们输入橙子,会得到一个负值;如果我们输入柠檬,会得到一个正值。有了这些知识,我们就可以计算神经网络在这个数据集上的准确性:
Pythonfrom collections import Counter evaluation = Counter() for point in zip(oranges_x, oranges_y): res = p(point) if res < 0: # 橙子期望为负 evaluation['corrects'] += 1 else: evaluation['wrongs'] += 1 for point in zip(lemons_x, lemons_y): res = p(point) if res >= 0: # 柠檬期望为正 evaluation['corrects'] += 1 else: evaluation['wrongs'] += 1 print(evaluation)
Counter({'corrects': 200}) # 假设初始生成了 100 个橙子和 100 个柠檬
计算是如何进行的?我们将输入值与权重相乘,得到负值和正值。让我们检查一下如果计算结果为 0 会得到什么:
我们可以将这个方程转换为:
我们可以将其与直线的通用形式进行比较:
其中:
-
m 是线的斜率或梯度。
-
c 是线的 y 轴截距。
-
x 是函数的自变量。
我们可以很容易地看到,我们的方程对应于直线的定义,斜率(也称为梯度)m 是
,而 c 等于 0。
这是一条分隔橙子和柠檬的直线,被称为决策边界。
我们用以下 Python 程序可视化这一点:
Pythonimport time # 未使用,可以删除 import matplotlib.pyplot as plt import numpy as np # 确保导入 numpy X = np.arange(0, 8) # 修正:X的范围应与数据范围匹配,调整为0到8 fig, ax = plt.subplots() ax.scatter(oranges_x, oranges_y, c="orange", label="oranges") ax.scatter(lemons_x, lemons_y, c="y", label="lemons") # 决策边界的斜率计算 slope = -p.weights[0] / p.weights[1] # slope = 0.45 / 0.5使用感知器 p 的权重 ax.plot(X, slope * X, linewidth=2, label="decision boundary") # 添加标签 ax.grid() ax.legend() # 显示图例 plt.show() print(slope)
0.9
训练神经网络
正如我们在上一节中提到的:我们没有训练我们的网络。我们调整了权重到我们已知会形成分割线的值。我们现在将演示训练我们简单神经网络所需的东西。
在我们开始这项任务之前,我们将在以下 Python 程序中将我们的数据分成训练数据和测试数据。通过将
random_state
设置为 42,我们将获得每次运行相同的输出,这对于调试目的可能很有益。Pythonfrom sklearn.model_selection import train_test_split import random import numpy as np # 确保导入 numpy oranges = list(zip(oranges_x, oranges_y)) # 假设 oranges_x 和 oranges_y 已在之前生成 lemons = list(zip(lemons_x, lemons_y)) # 给橙子打标签 0,柠檬打标签 1: labelled_data = list(zip(oranges + lemons, [0] * len(oranges) + [1] * len(lemons))) random.shuffle(labelled_data) # 打乱数据 data, labels = zip(*labelled_data) # 解包数据和标签 res = train_test_split(data, labels, train_size=0.8, # 80% 作为训练数据 test_size=0.2, # 20% 作为测试数据 random_state=42) # 固定随机种子以保证结果可复现 train_data, test_data, train_labels, test_labels = res print(train_data[:10], train_labels[:10])
[(2.592320569178846, 5.623712204925406), (4.7943502284049355, 0.8839613414681706), (2.1239534889189637, 5.377962359316873), (4.130183870483639, 3.2036358839244397), (2.5700607722439957, 3.4894903329620393), (1.1874742907020708, 4.248237496795156), (4.975409937616054, 3.258818001021547), (2.4858113049930375, 3.778544332039814), (0.759896779289841, 4.699741038079466), (1.3275488108562907, 4.204176294559159)] [1, 0, 1, 0, 1, 1, 0, 1, 1, 1]
由于我们从两个任意权重开始,我们不能期望结果是正确的。对于某些点(水果),它可能会返回正确的值,即柠檬为 1,橙子为 0。如果我们得到错误的结果,我们必须纠正我们的权重值。首先,我们必须计算误差。误差是目标值或期望值(
target_result
)与计算值(calculated_result
)之间的差值。有了这个误差,我们必须通过一个增量值来调整权重值,即 。如果误差 e 为 0,即目标结果等于计算结果,我们就不需要做任何事情。对于这些输入值,网络是完美的。如果误差不等于 0,我们必须改变权重。我们必须通过向它们添加小值来改变权重。这些值可以是正的也可以是负的。我们改变权重值的量取决于误差和输入值。假设 x_1 和 x_2 是输入。在这种情况下,结果只取决于输入 x_2。这反过来意味着我们可以通过仅仅改变 w_2 来最小化误差。如果误差是负的,我们将不得不向其添加一个负值;如果误差是正的,我们将不得不向其添加一个正值。由此我们可以理解,无论输入值是什么,我们都可以将它们与误差相乘,然后得到可以添加到权重的值。还有一件事:这样做我们会学得太快。我们有许多样本,每个样本都应该只稍微改变权重。因此,我们必须将这个结果乘以一个学习率(
self.learning_rate
)。学习率用于控制权重更新的速度。学习率小会导致训练过程漫长,学习率大则有最终得到次优权重值的风险。我们将在关于反向传播的章节中更详细地了解这一点。我们现在准备编写用于调整权重(即训练网络)的代码。为此,我们在
Perceptron
类中添加一个adjust
方法。这个方法的任务是纠正误差。Pythonimport numpy as np from collections import Counter class Perceptron: def __init__(self, weights, learning_rate=0.1): """ 'weights' 可以是一个 numpy 数组、列表或元组,包含权重的实际值。 输入值的数量由 'weights' 的长度间接定义。 """ self.weights = np.array(weights) self.learning_rate = learning_rate @staticmethod def unit_step_function(x): if x < 0: return 0 else: return 1 def __call__(self, in_data): weighted_input = self.weights * in_data weighted_sum = weighted_input.sum() #print(in_data, weighted_input, weighted_sum) return Perceptron.unit_step_function(weighted_sum) def adjust(self, target_result, calculated_result, in_data): if not isinstance(in_data, np.ndarray): # 修正:使用 isinstance in_data = np.array(in_data) error = target_result - calculated_result if error != 0: correction = error * in_data * self.learning_rate self.weights += correction #print(target_result, calculated_result, error, in_data, correction, self.weights) def evaluate(self, data, labels): evaluation = Counter() for index in range(len(data)): # 注意:p(data[index]) 返回 0 或 1。round(..., 0) 在这里是多余的。 # int() 转换即可。 label = int(p(data[index])) if label == labels[index]: evaluation["correct"] += 1 else: evaluation["wrong"] += 1 return evaluation p = Perceptron(weights=[0.1, 0.1], # 初始权重 learning_rate=0.3) # 学习率 # 训练感知器 for index in range(len(train_data)): p.adjust(train_labels[index], p(train_data[index]), train_data[index]) # 评估训练数据 evaluation = p.evaluate(train_data, train_labels) print(evaluation.most_common()) # 评估测试数据 evaluation = p.evaluate(test_data, test_labels) print(evaluation.most_common()) print(p.weights)
[('correct', 160)] [('correct', 40)] [-1.68135341 2.07512397]
无论是学习数据还是测试数据,我们都只有正确的值,这意味着我们的网络能够自动且成功地学习!
我们用以下程序可视化决策边界:
Pythonimport matplotlib.pyplot as plt import numpy as np X = np.arange(0, 7) # 绘制直线的 X 范围 fig, ax = plt.subplots() # 从训练数据中分离柠檬和橙子,以便可视化 lemons = [train_data[i] for i in range(len(train_data)) if train_labels[i] == 1] lemons_x, lemons_y = zip(*lemons) oranges = [train_data[i] for i in range(len(train_data)) if train_labels[i] == 0] oranges_x, oranges_y = zip(*oranges) ax.scatter(oranges_x, oranges_y, c="orange", label="Oranges") # 添加标签 ax.scatter(lemons_x, lemons_y, c="y", label="Lemons") # 添加标签 w1 = p.weights[0] w2 = p.weights[1] m = -w1 / w2 # 计算决策边界的斜率 ax.plot(X, m * X, label="Decision Boundary", color="g", linewidth=2) # 绘制决策边界,增加颜色和线宽 ax.legend() # 显示图例 plt.show() print(p.weights)
[-1.68135341 2.07512397]让我们来看看算法“运动”起来的样子。
Pythonimport numpy as np import matplotlib.pyplot as plt import matplotlib.cm as cm # 导入颜色映射模块 p = Perceptron(weights=[0.1, 0.1], # 初始权重 learning_rate=0.3) # 学习率 number_of_colors = 7 colors = cm.rainbow(np.linspace(0, 1, number_of_colors)) # 生成彩虹色系 fig, ax = plt.subplots() ax.set_xticks(range(8)) # 设置 X 轴刻度 ax.set_ylim([-2, 8]) # 设置 Y 轴范围 counter = 0 for index in range(len(train_data)): old_weights = p.weights.copy() # 复制旧权重以检查是否发生变化 p.adjust(train_labels[index], p(train_data[index]), train_data[index]) if not np.array_equal(old_weights, p.weights): # 如果权重发生了变化 color = "orange" if train_labels[index] == 0 else "y" # 根据标签选择颜色 ax.scatter(train_data[index][0], train_data[index][1], color=color) ax.annotate(str(counter), # 标记改变权重的点 (train_data[index][0], train_data[index][1]), textcoords="offset points", xytext=(0,10), ha='center') # 调整标注位置 m = -p.weights[0] / p.weights[1] # 计算新的决策边界斜率 print(index, f"{m:.4f}", p.weights, train_data[index]) # 打印当前索引、斜率、权重和数据点 ax.plot(X, m * X, label=f"Line {counter}", color=colors[counter % number_of_colors], linewidth=1) # 绘制决策边界并添加标签 counter += 1 ax.legend() # 显示图例 plt.show()
1 -3.0400 [-1.45643048 -0.4790835 ] (5.188101611742407, 1.930278325463612) 2 0.5906 [-0.73406347 1.24291557] (2.4078900359381787, 5.739996893315745) 18 6.7005 [-2.03694068 0.30399756] (4.342924008657758, 3.129726697580847) 20 0.5044 [-0.87357998 1.73188666] (3.877868972161467, 4.759630340827767) 27 2.7419 [-2.39560903 0.87370868] (5.073430165416017, 2.8605932860372967) 31 0.8102 [-1.68135341 2.07512397] (2.38085207252672, 4.004717642222739)
图中每个点都会引起权重的变化。我们按照它们出现的顺序给它们编号,并显示相应的直线。通过这种方式,我们可以看到网络是如何“学习”的。
We will develop a simple neural network in this chapter of our tutorial. A network capable of separating two
classes, which are separable by a straight line in a 2-dimensional feature space.
LINE SEPARATION
Before we start programming a simple neural
network, we are going to develop a different concept.
We want to search for straight lines that separate two
points or two classes in a plane. We will only look at
straight lines going through the origin. We will look
at general straight lines later in the tutorial.
You could imagine that you have two attributes
describing an eddible object like a fruit for example:
"sweetness" and "sourness".
We could describe this by points in a two-
dimensional space. The A axis is used for the values
of sweetness and the y axis is correspondingly used
for the sourness values. Imagine now that we have
two fruits as points in this space, i.e. an orange at
position (3.5, 1.8) and a lemon at (1.1, 3.9).
We could define dividing lines to define the points which are more lemon-like and which are more orange-
like.
In the following diagram, we depict one lemon and one orange. The green line is separating both points. We
assume that all other lemons are above this line and all oranges will be below this line.
96
The green line is defined by
y = mx
where:
m is the slope or gradient of the line and x is the independent variable of the function.
p2
m =
x
p 1
This means that a point P ′ = (p ′ , p ′ ) is on this line, if the following condition is fulfilled:
1
2
mp ′ − p ′ = 0
1
2
The following Python program plots a graph depicting the previously described situation:
import matplotlib.pyplot as plt
import numpy as np
97
X = np.arange(0, 7)
fig, ax = plt.subplots()
ax.plot(3.5, 1.8, "or",
color="darkorange",
markersize=15)
ax.plot(1.1, 3.9, "oy",
markersize=15)
point_on_line = (4, 4.5)
ax.plot(1.1, 3.9, "oy", markersize=15)
# calculate gradient:
m = point_on_line[1] / point_on_line[0]
ax.plot(X, m * X, "g-", linewidth=3)
plt.show()
It is clear that a point A = (a 1, a 2) is not on the line, if m ⋅ a 1 − a 2 is not equal to 0. We want to know more.
We want to know, if a point is above or below a straight line.
98
If a point B = (b 1, b 2) is below this line, there must be a δ B > 0 so that the point (b1, b 2 + δB) will be on the
line.
This means that
m ⋅ b1 − (b2 + δ B) = 0
which can be rearranged to
m ⋅ b 1 − b 2 = δB
Finally, we have a criteria for a point to be below the line. m ⋅ b 1 − b2 is positve, because δ B is positive.
The reasoning for "a point is above the line" is analogue: If a point A = (a1, a 2) is above the line, there must
be a δA > 0 so that the point (a1, a 2 − δ A) will be on the line.
This means that
m ⋅ a1 − (a2 − δ A) = 0
which can be rearranged to
m ⋅ a 1 − a2 = − δ A
99
In summary, we can say: A point P(p1, p 2) lies
•••below the straight line if m ⋅ p 1 − p 2 > 0
on the straight line if m ⋅ p 1 − p 2 = 0
above the straight line if m ⋅ p 1 − p 2 < 0
We can now verify this on our fruits. The lemon has the coordinates (1.1, 3.9) and the orange the coordinates
3.5, 1.8. The point on the line, which we used to define our separation straight line has the values (4, 4.5). So
m is 4.5 divides by 4.
lemon = (1.1, 3.9)
orange = (3.5, 1.8)
m = 4.5 / 4
# check if orange is below the line,
# positive value is expected:
print(orange[0] * m - orange[1])
# check if lemon is above the line,
# negative value is expected:
print(lemon[0] * m - lemon[1])
2.1375
-2.6624999999999996
We did not calculate the green line using mathematical formulas or methods, but arbitrarily determined it by
visual judgement. We could have chosen other lines as well.
The following Python program calculates and renders a bunch of lines. All going through the origin, i.e. the
point (0, 0). The red ones are completely unusable for the purpose of separating the two fruits, because in
these cases both the lemon and the orange are on the same side of the straight line. However, it is obvious that
even the green ones might not be too useful if we have more than these two fruits. Some lemons might be
sweeter and some oranges can be quite sour.
import numpy as np
import matplotlib.pyplot as plt
def create_distance_function(a, b, c):
""" 0 = ax + by + c """
def distance(x, y):
"""
returns tuple (d, pos)
d is the distance
100
If pos == -1 point is below the line,
0 on the line and +1 if above the line
"""
nom = a * x + b * y + c
if nom == 0:
pos = 0
elif (nom<0 and b<0) or (nom>0 and b>0):
pos = -1
else:
pos = 1
return (np.absolute(nom) / np.sqrt( a ** 2 + b ** 2),return distance
orange = (4.5, 1.8)
lemon = (1.1, 3.9)
fruits_coords = [orange, lemon]
fig, ax = plt.subplots()
ax.set_xlabel("sweetness")
ax.set_ylabel("sourness")
x_min, x_max = -1, 7
y_min, y_max = -1, 8
ax.set_xlim([x_min, x_max])
ax.set_ylim([y_min, y_max])
X = np.arange(x_min, x_max, 0.1)
step = 0.05
for x in np.arange(0, 1+step, step):
slope = np.tan(np.arccos(x))
dist4line1 = create_distance_function(slope, -1, 0)
Y = slope * X
results = []
for point in fruits_coords:
results.append(dist4line1(*point))
if (results[0][1] != results[1][1]):
ax.plot(X, Y, "g-", linewidth=0.8, alpha=0.9)
else:
ax.plot(X, Y, "r-", linewidth=0.8, alpha=0.9)
size = 10
for (index, (x, y)) in enumerate(fruits_coords):
if index== 0:
ax.plot(x, y, "o",
color="darkorange",
markersize=size)
pos)
101
else:
ax.plot(x, y, "oy",
markersize=size)
plt.show()
Basically, we have carried out a classification based on our dividing line. Even if hardly anyone would
describe this as such.
It is easy to imagine that we have more lemons and oranges with slightly different sourness and sweetness
values. This means we have a class of lemons ( class1 ) and a class of oranges class2 . This is depicted
in the following diagram.
102
We are going to "grow" oranges and lemons with a Python program. We will create these two classes by
randomly creating points within a circle with a defined center point and radius. The following Python code
will create the classes:
import numpy as np
import matplotlib.pyplot as plt
def points_within_circle(radius,
center=(0, 0),
number_of_points=100):
center_x, center_y = center
r = radius * np.sqrt(np.random.random((number_of_points,)))
theta = np.random.random((number_of_points,)) * 2 * np.pi
x = center_x + r * np.cos(theta)
y = center_y + r * np.sin(theta)
return x, y
X = np.arange(0, 8)
fig, ax = plt.subplots()
oranges_x, oranges_y = points_within_circle(1.6, (5, 2), 100)
lemons_x, lemons_y = points_within_circle(1.9, (2, 5), 100)
ax.scatter(oranges_x,
oranges_y,
c="orange",
label="oranges")
ax.scatter(lemons_x,
103
lemons_y,
c="y",
label="lemons")
ax.plot(X, 0.9 * X, "g-", linewidth=2)
ax.legend()
ax.grid()
plt.show()
The dividing line was again arbitrarily set by eye. The question arises how to do this systematically? We are
still only looking at straight lines going through the origin, which are uniquely defined by its slope. the
following Python program calculates a dividing line by going through all the fruits and dynamically adjusts
the slope of the dividing line we want to calculate. If a point is above the line but should be below the line, the
slope will be increment by the value of learning_rate . If the point is below the line but should be above
the line, the slope will be decremented by the value of learning_rate .
import numpy as np
import matplotlib.pyplot as plt
from itertools import repeat
from random import shuffle
X = np.arange(0, 8)
fig, ax = plt.subplots()
ax.scatter(oranges_x,
oranges_y,
c="orange",
label="oranges")
ax.scatter(lemons_x,
104
lemons_y,
c="y",
label="lemons")
fruits = list(zip(oranges_x,
oranges_y,
repeat(0, len(oranges_x))))
fruits += list(zip(lemons_x,
lemons_y,
repeat(1, len(oranges_x))))
shuffle(fruits)
def adjust(learning_rate=0.3, slope=0.3):
line = None
counter = 0
for x, y, label in fruits:
res = slope * x - y
#print(label, res)
if label == 0 and res < 0:
# point is above line but should be below
# => increment slope
slope += learning_rate
counter += 1
ax.plot(X, slope * X,
linewidth=2, label=str(counter))
elif label == 1 and res > 0:
# point is below line but should be above
# => decrement slope
#print(res, label)
slope -= learning_rate
counter += 1
ax.plot(X, slope * X,
linewidth=2, label=str(counter))
return slope
slope = adjust()
ax.plot(X,
slope * X,
linewidth=2)
ax.legend()
ax.grid()
plt.show()
105
print(slope)
[<matplotlib.lines.Line2D object at 0x7f53b0a22c50>]
Let's start with a different slope from the 'lemon side':
X = np.arange(0, 8)
fig, ax = plt.subplots()
ax.scatter(oranges_x,
oranges_y,
c="orange",
label="oranges")
ax.scatter(lemons_x,
lemons_y,
c="y",
label="lemons")
slope = adjust(learning_rate=0.2, slope=3)
ax.plot(X,
slope * X,
linewidth=2)
ax.legend()
ax.grid()
plt.show()
print(slope)
106
0.9999999999999996
A SIMPLE NEURAL NETWORK
We were capable of separating the two classes with a straight line. One might wonder what this has to do with
neural networks. We will work out this connection below.
We are going to define a neural network to classify the previous data sets. Our neural network will only
consist of one neuron. A neuron with two input values, one for 'sourness' and one for 'sweetness'.
The two input values - called in_data in our Python program below - have to be weighted by weight
values. So solve our problem, we define a Perceptron class. An instance of the class is a Perceptron (or
Neuron). It can be initialized with the input_length, i.e. the number of input values, and the weights, which can
be given as a list, tuple or an array. If there are no values for the weights given or the parameter is set to None,
we will initialize the weights to 1 / input_length.
In the following example choose -0.45 and 0.5 as the values for the weights. This is not the normal way to do
it. A Neural Network calculates the weights automatically during its training phase, as we will learn later.
import numpy as np
107
class Perceptron:
def __init__(self, weights):
"""
'weights' can be a numpy array, list or a tuple with the
actual values of the weights. The number of input values
is indirectly defined by the length of 'weights'
"""
self.weights = np.array(weights)
def __call__(self, in_data):
weighted_input = self.weights * in_data
weighted_sum = weighted_input.sum()
return weighted_sum
p = Perceptron(weights=[-0.45, 0.5])
for point in zip(oranges_x[:10], oranges_y[:10]):
res = p(point)
print(res, end=", ")
for point in zip(lemons_x[:10], lemons_y[:10]):
res = p(point)
print(res, end=", ")
-1.8131460150609238, -1.1931285955719209, -1.3127632381850327,
-1.3925163810790897, -0.7522874009031233, -0.8402958901009828,
-1.9330506389030604, -1.490534974734101, -0.4441170096959772, -1.9
942817372340516, 1.998076257605724, 1.1512784858148413, 2.51418870
799987, 0.4867012212497872, 1.7962680593822624, 0.875162742271260
9, 1.5455925862569528, 1.6976576197574347, 1.4467637066140102, 1.4
634541513290587,
We can see that we get a negative value, if we input an orange and a posive value, if we input a lemon. With
this knowledge, we can calculate the accuracy of our neural network on this data set:
from collections import Counter
evaluation = Counter()
for point in zip(oranges_x, oranges_y):
res = p(point)
if res < 0:
evaluation['corrects'] += 1
else:
evaluation['wrongs'] += 1
108
for point in zip(lemons_x, lemons_y):
res = p(point)
if res >= 0:
evaluation['corrects'] += 1
else:
evaluation['wrongs'] += 1
print(evaluation)
Counter({'corrects':200})
How does the calculation work? We multiply the input values with the weights and get negative and positive
values. Let us examine what we get, if the calculation results in 0:
w 1 ⋅ x 1 + w 2 ⋅ x 2 = 0
We can change this equation into
w1
x2 = −
⋅ x 1
w2
We can compare this with the general form of a straight line
y = m ⋅ x + c
where:
• m is the slope or gradient of the line.
• c is the y-intercept of the line.
• x is the independent variable of the function.
We can easily see that our equation corresponds to the definition of a line and the slope (aka gradient) m is
w1
− and c is equal to 0.
w2
This is a straight line separating the oranges and lemons, which is called the decision boundary.
We visualize this with the following Python program:
import time
import matplotlib.pyplot as plt
slope = 0.1
X = np.arange(0, 8)
109
fig, ax = plt.subplots()
ax.scatter(oranges_x,
oranges_y,
c="orange",
label="oranges")
ax.scatter(lemons_x,
lemons_y,
c="y",
label="lemons")
slope = 0.45 / 0.5
ax.plot(X, slope * X,
linewidth=2)
ax.grid()
plt.show()
print(slope)
0.9
TRAINING A NEURAL NETWORK
As we mentioned in the previous section: We didn't train our network. We have adjusted the weights to values
that we know would form a dividing line. We want to demonstrate now, what is necessary to train our simple
neural network.
Before we start with this task, we will separate our data into training and test data in the following Python
program. By setting the random_state to the value 42 we will have the same output for every run, which can
be benifial for debugging purposes.
110
from sklearn.model_selection import train_test_split
import random
oranges = list(zip(oranges_x, oranges_y))
lemons = list(zip(lemons_x, lemons_y))
# labelling oranges with 0 and lemons with 1:
labelled_data = list(zip(oranges + lemons,
[0] * len(oranges) + [1] * len(lemons)))
random.shuffle(labelled_data)
data, labels = zip(*labelled_data)
res = train_test_split(data, labels,
train_size=0.8,
test_size=0.2,
random_state=42)
train_data, test_data, train_labels, test_labels = res
print(train_data[:10], train_labels[:10])
[(2.592320569178846, 5.623712204925406), (4.7943502284049355, 0.88
39613414681706), (2.1239534889189637, 5.377962359316873), (4.13018
3870483639, 3.2036358839244397), (2.5700607722439957, 3.4894903329
620393), (1.1874742907020708, 4.248237496795156), (4.9754099376160
54, 3.258818001021547), (2.4858113049930375, 3.778544332039814),
(0.759896779289841, 4.699741038079466), (1.3275488108562907, 4.204
176294559159)] [1, 0, 1, 0, 1, 1, 0, 1, 1, 1]
As we start with two arbitrary weights, we cannot expect the result to be correct. For some points (fruits) it
may return the proper value, i.e. 1 for a lemon and 0 for an orange. In case we get the wrong result, we have to
correct our weight values. First we have to calculate the error. The error is the difference between the target or
expected value ( target_result ) and the calculated value ( calculated_result ). With this error
we have to adjust the weight values with an incremental value, i.e. w1 = w 1 + Δw 1 and w2 = w 2 + Δw 2
111
If the error e is 0, i.e. the target result is equal to the calculated result, we don't have to do anything. The
network is perfect for these input values. If the error is not equal, we have to change the weights. We have to
change the weights by adding small values to them. These values may be positive or negative. The amount we
have a change a weight value depends on the error and on the input value. Let us assume, x = 0 and x > 0.
1 2In this case the result in this case solely results on the input x 2. This on the other hand means that we can
minimize the error by changing solely w 2. If the error is negative, we will have to add a negative value to it,
and if the error is positive, we will have to add a positive value to it. From this we can understand that
whatever the input values are, we can multiply them with the error and we get values, we can add to the
weights. One thing is still missing: Doing this we would learn to fast. We have many samples and each sample
should only change the weights a little bit. Therefore we have to multiply this result with a learning rate
( self.learning_rate ). The learning rate is used to control how fast the weights are updated. Small
values for the learning rate result in a long training process, larger values bear the risk of ending up in sub-
optimal weight values. We will have a closer look at this in our chapter on backpropagation.
We are ready now to write the code for adapting the weights, which means training the network. For this
purpose, we add a method 'adjust' to our Perceptron class. The task of this method is to crrect the error.
import numpy as np
from collections import Counter
class Perceptron:
def __init__(self,
weights,
learning_rate=0.1):
"""
'weights' can be a numpy array, list or a tuple with the
actual values of the weights. The number of input values
is indirectly defined by the length of 'weights'
"""
self.weights = np.array(weights)
self.learning_rate = learning_rate
@staticmethod
def unit_step_function(x):
if x < 0:
return 0
else:
return 1
def __call__(self, in_data):
weighted_input = self.weights * in_data
weighted_sum = weighted_input.sum()
#print(in_data, weighted_input, weighted_sum)
112
return Perceptron.unit_step_function(weighted_sum)
def adjust(self,
target_result,
calculated_result,
in_data):
if type(in_data) != np.ndarray:
in_data = np.array(in_data) #
error = target_result - calculated_result
if error != 0:
correction = error * in_data * self.learning_rate
self.weights += correction
#print(target_result, calculated_result, error, in_dat
a, correction, self.weights)
def evaluate(self, data, labels):
evaluation = Counter()
for index in range(len(data)):
label = int(round(p(data[index]),0))
if label == labels[index]:
evaluation["correct"] += 1
else:
evaluation["wrong"] += 1
return evaluation
p = Perceptron(weights=[0.1, 0.1],
learning_rate=0.3)
for index in range(len(train_data)):
p.adjust(train_labels[index],
p(train_data[index]),
train_data[index])
evaluation = p.evaluate(train_data, train_labels)
print(evaluation.most_common())
evaluation = p.evaluate(test_data, test_labels)
print(evaluation.most_common())
print(p.weights)
[('correct',[('correct',[-1.68135341160)]
40)]
2.07512397]
113
Both on the learning and on the test data, we have only correct values, i.e. our network was capable of learning
automatically and successfully!
We visualize the decision boundary with the following program:
import matplotlib.pyplot as plt
import numpy as np
X = np.arange(0, 7)
fig, ax = plt.subplots()
lemons = [train_data[i] for i in range(len(train_data)) if train_l
abels[i] == 1]
lemons_x, lemons_y = zip(*lemons)
oranges = [train_data[i] for i in range(len(train_data)) if trai
n_labels[i] == 0]
oranges_x, oranges_y = zip(*oranges)
ax.scatter(oranges_x, oranges_y, c="orange")
ax.scatter(lemons_x, lemons_y, c="y")
w1 = p.weights[0]
w2 = p.weights[1]
m = -w1 / w2
ax.plot(X, m * X, label="decision boundary")
ax.legend()
plt.show()
print(p.weights)
[-1.68135341
2.07512397]
114
Let us have a look on the algorithm "in motion".
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
p = Perceptron(weights=[0.1, 0.1],
learning_rate=0.3)
number_of_colors = 7
colors = cm.rainbow(np.linspace(0, 1, number_of_colors))
fig, ax = plt.subplots()
ax.set_xticks(range(8))
ax.set_ylim([-2, 8])
counter = 0
for index in range(len(train_data)):
old_weights = p.weights.copy()
p.adjust(train_labels[index],
p(train_data[index]),
train_data[index])
if not np.array_equal(old_weights, p.weights):
color = "orange" if train_labels[index] == 0 else
"y"
ax.scatter(train_data[index][0],
train_data[index][1],
color=color)
ax.annotate(str(counter),
(train_data[index][0], train_data[index][1]))
m = -p.weights[0] / p.weights[1]
print(index, m, p.weights, train_data[index])
ax.plot(X, m * X, label=str(counter), color=colors[counte
r])
counter += 1
ax.legend()
plt.show()
115
1 -3.0400347553192493 [-1.45643048 -0.4790835 ] (5.18810161174240
7, 1.930278325463612)
2 0.5905980182798966 [-0.73406347 1.24291557] (2.407890035938178
7, 5.739996893315745)
18 6.70051650445074 [-2.03694068 0.30399756] (4.342924008657758,
3.129726697580847)
20 0.5044094409795936 [-0.87357998 1.73188666] (3.87786897216146
7, 4.759630340827767)
27 2.7418853617419434 [-2.39560903 0.87370868] (5.07343016541601
7, 2.8605932860372967)
31 0.8102423930878537 [-1.68135341 2.07512397] (2.3808520725267
2, 4.004717642222739)
Each of the points in the diagram above cause a change in the weights. We see them numbered in the order of
their appearance and the corresponding straight line. This way we can see how the networks "learns". -
-
引言
在我们的机器学习教程的上一章中,我们介绍了神经网络的基本思想。
我们指出了生物学中神经元与神经网络之间的相似性。我们还介绍了非常小型的人工神经网络,并引入了决策边界和 XOR 问题。
到目前为止,在我们介绍的简单示例中,我们看到权重是神经网络的核心组成部分。在开始编写多层神经网络之前,我们需要仔细研究一下权重。我们必须了解如何初始化权重以及如何有效地将权重与输入值相乘。
在接下来的章节中,我们将用 Python 设计一个包含三层的神经网络,即输入层、隐藏层和输出层。您可以在下面的图中看到这种神经网络结构。我们有一个包含三个节点 \(i_1\),\(i_2\),\(i_3\) 的输入层。这些节点接收相应的输入值 \(x_1\),\(x_2\),\(x_3\)。中间或隐藏层有四个节点 \(h_1\),\(h_2\),\(h_3\),\(h_4\)。这一层的输入源自输入层。我们很快就会讨论其机制。最后,我们的输出层由两个节点 \(o_1\),\(o_2\) 组成。
输入层与其他层不同。输入层的节点是被动的。这意味着输入神经元不会改变数据,即在这种情况下不使用权重。它们接收一个单一值并将其复制到多个输出中。
输入层由节点 \(i_1\),\(i_2\),\(i_3\) 组成。原则上,输入是一个一维向量,例如 (2,4,11)。一维向量在 NumPy 中表示如下:
Pythonimport numpy as np input_vector = np.array([2, 4, 11]) print(input_vector)
输出:
[ 2 4 11]
在我们稍后编写的算法中,我们必须将其转置为列向量,即一个只有一列的二维数组:
Pythonimport numpy as np input_vector = np.array([2, 4, 11]) input_vector = np.array(input_vector, ndmin=2).T print("The input vector:\n", input_vector) print("The shape of this vector: ", input_vector.shape)
输出:
The input vector: [[ 2] [ 4] [11]] The shape of this vector: (3, 1)
权重与矩阵
我们网络图中的每条箭头都有一个相关的权重值。现在我们只看输入层和输出层之间的箭头。
进入节点 \(i_1\) 的值 \(x_1\) 将根据权重值进行分配。在下面的图中,我们添加了一些示例值。使用这些值,隐藏层节点 (\(h_1\),\(h_2\),\(h_3\),\(h_4\)) 的输入值 (I\(h_1\),I\(h_2\),I\(h_3\),I\(h_4\)) 可以这样计算:
\(Ih_1 = 0.81 * 0.5 + 0.12 * 1 + 0.92 * 0.8\)
\(Ih_2 = 0.33 * 0.5 + 0.44 * 1 + 0.72 * 0.8\)
\(Ih_3 = 0.29 * 0.5 + 0.22 * 1 + 0.53 * 0.8\)
\(Ih_4 = 0.37 * 0.5 + 0.12 * 1 + 0.27 * 0.8\)
熟悉矩阵和矩阵乘法的人会明白这最终归结于什么。我们将重新绘制我们的网络并用 \(w_ij\) 表示权重:
为了有效地执行所有必要的计算,我们将权重排列成一个权重矩阵。
我们上面图中的权重构成了一个数组,我们将在我们的神经网络类中将其命名为 'weights_in_hidden'。这个名称应该表明这些权重连接着输入节点和隐藏节点,也就是说,它们位于输入层和隐藏层之间。我们还会将其缩写为 'wih'。隐藏层和输出层之间的权重矩阵将表示为 "who"。
现在我们已经定义了权重矩阵,我们必须进行下一步。我们必须将矩阵
wih
乘以输入向量。顺便说一句,这正是我们在前面的示例中手动完成的。对于隐藏层和输出层之间的 'who' 矩阵,我们也有类似的情况。因此,来自节点 \(o_1\) 和 \(o_2\) 的输出 \(z_1\) 和 \(z_2\) 也可以通过矩阵乘法计算:
您可能已经注意到我们之前的计算中缺少一些东西。我们在介绍性的“从零开始用 Python 构建神经网络”一章中展示过,我们必须对每个这些和应用一个激活函数或阶跃函数 Φ。
下图描绘了整个计算流程,即矩阵乘法和随后的激活函数应用。
矩阵
wih
和输入节点值 \(x_1\),\(x_2\),\(x_3\) 矩阵之间的矩阵乘法计算出将传递给激活函数的输出。最终输出 \(y_1\),\(y_2\),\(y_3\),\(y_4\) 是权重矩阵
who
的输入:尽管处理方式完全类似,但我们也将详细研究隐藏层和输出层之间发生的事情:
初始化权重矩阵
在训练神经网络之前,需要做出的重要选择之一是初始化权重矩阵。当我们开始时,我们对可能的权重一无所知。那么,我们可以从任意值开始吗?
正如我们所看到的,除了输入节点之外,所有节点的输入都是通过对以下求和应用激活函数来计算的:
(其中 n 是前一层中的节点数,y_j 是到下一层节点的输入)
我们可以很容易地看到,将所有权重值设置为 0 并不是一个好主意,因为在这种情况下,这个求和的结果将始终为零。这意味着我们的网络将无法学习。这是最糟糕的选择,但将权重矩阵初始化为全 1 也是一个糟糕的选择。
权重矩阵的值应该随机选择,而不是任意选择。通过选择随机正态分布,我们打破了可能的对称情况,这对于学习过程来说可能并且通常是糟糕的。
有多种方法可以随机初始化权重矩阵。我们将介绍的第一种是来自
numpy.random
的均匀函数。它创建在半开区间 [low,high) 内均匀分布的样本,这意味着包含 low 但不包含 high。给定区间内的每个值被 'uniform' 抽取的可能性相同。Pythonimport numpy as np number_of_samples = 1200 low = -1 high = 0 s = np.random.uniform(low, high, number_of_samples)
Python# all values of s are within the half open interval [-1, 0) : print(np.all(s >= -1) and np.all(s < 0))
输出:
True
在我们的上一个示例中,使用
uniform
函数创建的样本的直方图如下所示:Pythonimport matplotlib.pyplot as plt plt.hist(s) plt.show()
我们将看的下一个函数是
numpy.random
中的binomial
:binomial(n, p, size=None)
它从具有指定参数的二项分布中抽取样本,n 次试验和成功概率 p,其中 n 是一个大于等于 0 的整数,p 是区间 [0,1] 中的浮点数。(n 可以输入为浮点数,但在使用时会被截断为整数)。
Pythons = np.random.binomial(100, 0.5, 1200) plt.hist(s) plt.show()
我们喜欢创建正态分布的随机数,但这些数字必须有界。
np.random.normal()
不提供任何边界参数,因此不适用。我们可以为此目的使用
scipy.stats
中的truncnorm
。这种分布的标准形式是截断到范围 [a,b] 的标准正态分布——请注意,a 和 b 是在标准正态分布的域上定义的。要转换特定均值和标准差的剪切值,请使用:
Pythonfrom scipy.stats import truncnorm s = truncnorm(a=-2/3., b=2/3., scale=1, loc=0).rvs(size=1000) plt.hist(s) plt.show()
truncnorm
函数使用起来比较困难。为了简化操作,我们将在下面定义一个名为truncated_normal
的函数,以便于完成此任务:Pythondef truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm( (low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) X = truncated_normal(mean=0, sd=0.4, low=-0.5, upp=0.5) s = X.rvs(10000) plt.hist(s) plt.show()
更多示例:
PythonX1 = truncated_normal(mean=2, sd=1, low=1, upp=10) X2 = truncated_normal(mean=5.5, sd=1, low=1, upp=10) X3 = truncated_normal(mean=8, sd=1, low=1, upp=10) import matplotlib.pyplot as plt fig, ax = plt.subplots(3, sharex=True) ax[0].hist(X1.rvs(10000), density=True) ax[1].hist(X2.rvs(10000), density=True) ax[2].hist(X3.rvs(10000), density=True) plt.show()
现在我们将创建链接权重矩阵。
truncated_normal
非常适合此目的。最好从区间 中选择随机值,其中 n 表示输入节点的数量。
因此,我们可以使用以下代码创建我们的 "wih" 矩阵:
Pythonno_of_input_nodes = 3 no_of_hidden_nodes = 4 rad = 1 / np.sqrt(no_of_input_nodes) X = truncated_normal(mean=2, sd=1, low=-rad, upp=rad) wih = X.rvs((no_of_hidden_nodes, no_of_input_nodes)) wih
输出:
array([[-0.41379992, -0.24122842, -0.0303682 ], [ 0.07304837, -0.00160437, 0.0911987 ], [ 0.32405689, 0.5103896 , 0.23972997], [ 0.097932 , -0.06646741, 0.01359876]])
同样,我们现在可以定义 "who" 权重矩阵:
Pythonno_of_hidden_nodes = 4 no_of_output_nodes = 2 rad = 1 / np.sqrt(no_of_hidden_nodes) # this is the input in this layer! X = truncated_normal(mean=2, sd=1, low=-rad, upp=rad) who = X.rvs((no_of_output_nodes, no_of_hidden_nodes)) who
输出:
array([[ 0.15892038, 0.06060043, 0.35900184, 0.14202827], [-0.4758216 , 0.29563269, 0.46035026, -0.29673539]])
INTRODUCTION
We introduced the basic ideas about
neural networks in the previous chapter of
our machine learning tutorial.
We have pointed out the similarity
between neurons and neural networks in
biology. We also introduced very small
articial neural networks and introduced
decision boundaries and the XOR
problem.
In the simple examples we introduced so
far, we saw that the weights are the
essential parts of a neural network. Before
we start to write a neural network with multiple layers, we need to have a closer look at the weights.
We have to see how to initialize the weights and how to efficiently multiply the weights with the input values.
In the following chapters we will design a neural network in Python, which consists of three layers, i.e. the
input layer, a hidden layer and an output layer. You can see this neural network structure in the following
diagram. We have an input layer with three nodes i 1, i 2, i 3 These nodes get the corresponding input values
x 1, x 2, x 3. The middle or hidden layer has four nodes h 1, h2, h 3, h 4. The input of this layer stems from the
input layer. We will discuss the mechanism soon. Finally, our output layer consists of the two nodes o 1, o2
The input layer is different from the other layers. The nodes of the input layer are passive. This means that the
input neurons do not change the data, i.e. there are no weights used in this case. They receive a single value
and duplicate this value to their many outputs.
141
The input layer consists of the nodes i 1, i 2 and i 3. In principle the input is a one-dimensional vector, like (2, 4,
11). A one-dimensional vector is represented in numpy like this:
import numpy as np
input_vector = np.array([2, 4, 11])
print(input_vector)
[ 2
4 11]
In the algorithm, which we will write later, we will have to transpose it into a column vector, i.e. a two-
dimensional array with just one column:
import numpy as np
input_vector = np.array([2, 4, 11])
input_vector = np.array(input_vector, ndmin=2).T
print("The input vector:\n", input_vector)
print("The shape of this vector: ", input_vector.shape)
The input vector:
[[ 2]
[ 4]
[11]]
The shape of this vector:
(3, 1)
142
WEIGHTS AND MATRICES
Each of the arrows in our network diagram has an associated weight value. We will only look at the arrows
between the input and the output layer now.
The value x1 going into the node i 1 will be distributed according to the values of the weights. In the following
diagram we have added some example values. Using these values, the input values (Ih 1, Ih 2, Ih 3, Ih 4 into the
nodes (h1, h 2, h 3, h 4) of the hidden layer can be calculated like this:
Ih 1 = 0.81 ∗ 0.5 + 0.12 ∗ 1 + 0.92 ∗ 0.8
Ih 2 = 0.33 ∗ 0.5 + 0.44 ∗ 1 + 0.72 ∗ 0.8
Ih 3 = 0.29 ∗ 0.5 + 0.22 ∗ 1 + 0.53 ∗ 0.8
Ih 4 = 0.37 ∗ 0.5 + 0.12 ∗ 1 + 0.27 ∗ 0.8
Those familiar with matrices and matrix multiplication will see where it is boiling down to. We will redraw
our network and denote the weights with wij:
143
In order to efficiently execute all the necessary calaculations, we will arrange the weights into a weight matrix.
144
The weights in our diagram above build an array, which we will call 'weights_in_hidden' in our Neural
Network class. The name should indicate that the weights are connecting the input and the hidden nodes, i.e.
they are between the input and the hidden layer. We will also abbreviate the name as 'wih'. The weight matrix
between the hidden and the output layer will be denoted as "who".:
Now that we have defined our weight matrices, we have to take the next step. We have to multiply the matrix
wih the input vector. Btw. this is exactly what we have manually done in our previous example.
( y y y y 4
1
2
3
) =
(
w w w w 31
41
11
21
w w w w 22
42
32
12
w w w w 13
33
23
43
) ( x3
x1
x2
) =
(
w w w w 41 31 21 11 ⋅ ⋅ ⋅ ⋅ x x x x 1 1 1 1 + + + + w w w w 12 42 22 32 ⋅ ⋅ ⋅ ⋅ x2 x x2 x2 2 + + + + w w13 w w 43 23 33 ⋅ ⋅ ⋅ ⋅ x x x x 3
3
3
3
)
We have a similar situation for the 'who' matrix between hidden and output layer. So the output z and z from
1 2the nodes o 1 and o 2 can also be calculated with matrix multiplications:
( z1
z2
) =
(
wh wh 11
21 wh wh 22
12
wh23 wh13 wh wh 14
24
) ( y y y y 4
1
2
3
)
=
(
wh21 wh 11 ⋅ ⋅ y1 y 1 + + wh wh 22 12 ⋅ ⋅ y y 2 2 + + wh wh 13 23 ⋅ ⋅ y3 y 3 + + wh wh 24 14 ⋅ ⋅ y y 4
4
)
You might have noticed that something is missing in our previous calculations. We showed in our introductory
145
chapter Neural Networks from Scratch in Python that we have to apply an activation or step function Φ on
each of these sums.
The following picture depicts the whole flow of calculation, i.e. the matrix multiplication and the succeeding
application of the activation function.
The matrix multiplication between the matrix wih and the matrix of the values of the input nodes x 1, x 2, x 3
calculates the output which will be passed to the activation function.
The final output y 1, y 2, y 3, y4 is the input of the weight matrix who:
Even though treatment is completely analogue, we will also have a detailled look at what is going on between
our hidden layer and the output layer:
146
INITIALIZING THE WEIGHT MATRICES
One of the important choices which have to be made before training a neural network consists in initializing
the weight matrices. We don't know anything about the possible weights, when we start. So, we could start
with arbitrary values?
As we have seen the input to all the nodes except the input nodes is calculated by applying the activation
function to the following sum:
n
y j = ∑ w ji ⋅ xi
i =1
(with n being the number of nodes in the previous layer and y j is the input to a node of the next layer)
We can easily see that it would not be a good idea to set all the weight values to 0, because in this case the
result of this summation will always be zero. This means that our network will be incapable of learning. This
is the worst choice, but initializing a weight matrix to ones is also a bad choice.
The values for the weight matrices should be chosen randomly and not arbitrarily. By choosing a random
normal distribution we have broken possible symmetric situations, which can and often are bad for the
learning process.
There are various ways to initialize the weight matrices randomly. The first one we will introduce is the unity
function from numpy.random. It creates samples which are uniformly distributed over the half-open interval
[low, high), which means that low is included and high is excluded. Each value within the given interval is
equally likely to be drawn by 'uniform'.
import numpy as np
number_of_samples = 1200
low = -1
high = 0
s = np.random.uniform(low, high, number_of_samples)
147
# all values of s are within the half open interval [-1, 0) :
print(np.all(s >= -1) and np.all(s < 0))
True
The histogram of the samples, created with the uniform function in our previous example, looks like this:
import matplotlib.pyplot as plt
plt.hist(s)
plt.show()
The next function we will look at is 'binomial' from numpy.binomial:
binomial(n, p, size=None)
It draws samples from a binomial distribution with specified parameters, n trials and probability p of
success where n is an integer >= 0 and p is a float in the interval [0,1]. ( n may be input as a float, but
it is truncated to an integer in use)
s = np.random.binomial(100, 0.5, 1200)
plt.hist(s)
plt.show()
148
We like to create random numbers with a normal distribution, but the numbers have to be bounded. This is not
the case with np.random.normal(), because it doesn't offer any bound parameter.
We can use truncnorm from scipy.stats for this purpose.
The standard form of this distribution is a standard normal truncated to the range [a, b] — notice that a and b
are defined over the domain of the standard normal. To convert clip values for a specific mean and standard
deviation, use:
a, b = (myclip_a - my_mean) / my_std, (myclip_b - my_mean) / my_std
from scipy.stats import truncnorm
s = truncnorm(a=-2/3., b=2/3., scale=1, loc=0).rvs(size=1000)
plt.hist(s)
plt.show()
149
The function 'truncnorm' is difficult to use. To make life easier, we define a function truncated_normal
in the following to fascilitate this task:
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm(
(low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd)
X = truncated_normal(mean=0, sd=0.4, low=-0.5, upp=0.5)
s = X.rvs(10000)
plt.hist(s)
plt.show()
Further examples:
150
X1 = truncated_normal(mean=2, sd=1, low=1, upp=10)
X2 = truncated_normal(mean=5.5, sd=1, low=1, upp=10)
X3 = truncated_normal(mean=8, sd=1, low=1, upp=10)
import matplotlib.pyplot as plt
fig, ax = plt.subplots(3, sharex=True)
ax[0].hist(X1.rvs(10000), density=True)
ax[1].hist(X2.rvs(10000), density=True)
ax[2].hist(X3.rvs(10000), density=True)
plt.show()
We will create the link weights matrix now. truncated_normal is ideal for this purpose. It is a good
idea to choose random values from within the interval
1
1
(−
,
)
√n √n
where n denotes the number of input nodes.
So we can create our "wih" matrix with:
no_of_input_nodes = 3
no_of_hidden_nodes = 4
rad = 1 / np.sqrt(no_of_input_nodes)
X = truncated_normal(mean=2, sd=1, low=-rad, upp=rad)
wih = X.rvs((no_of_hidden_nodes, no_of_input_nodes))
wih
151
Output:array([[-0.41379992, -0.24122842, -0.0303682 ],
[ 0.07304837, -0.00160437,
0.0911987 ],
[ 0.32405689, 0.5103896 ,
0.23972997],
[ 0.097932 , -0.06646741,
0.01359876]])
Similarly, we can now define the "who" weight matrix:
no_of_hidden_nodes = 4
no_of_output_nodes = 2
rad = 1 / np.sqrt(no_of_hidden_nodes)
# this is the input in thi
s layer!
X = truncated_normal(mean=2, sd=1, low=-rad, upp=rad)
who = X.rvs((no_of_output_nodes, no_of_hidden_nodes))
who
Output:array([[ 0.15892038,
0.06060043,
0.35900184, 0.14202827],
[-0.4758216 ,
0.29563269,
0.46035026, -0.29673539]]) -
神经网络类
在我们的神经网络教程上一章中,我们学习了关于权重最重要的事实。我们了解了它们是如何使用的以及如何在 Python 中实现它们。我们看到,权值与输入值的乘法可以通过应用矩阵乘法来使用 NumPy 数组完成。
然而,我们还没有在一个真实的神经网络环境中测试它们。我们必须首先创建这个环境。我们现在将在 Python 中创建一个实现神经网络的类。我们将循序渐进,以便一切都易于理解。
我们的类最基本的方法是:
-
__init__
: 用于初始化一个类,即我们将设置每一层的神经元数量并初始化权重矩阵。 -
run
: 应用于我们要分类的样本的方法。它将此样本应用于神经网络。我们可以说,我们“运行”网络来“预测”结果。这个方法在其他实现中通常被称为predict
。 -
train
: 这个方法将样本和相应的目标值作为输入。有了这些输入,它可以在必要时调整权重值。这意味着网络从输入中学习。从用户的角度来看,我们“训练”网络。例如,在 scikit-learn 中,这个方法被称为fit
。
我们将把
train
和run
方法的定义推迟到后面。权重矩阵应该在__init__
方法内部初始化。我们间接地这样做。我们定义一个方法create_weight_matrices
并在__init__
中调用它。这样,init
方法保持清晰。我们还将推迟向层中添加偏置节点。
下面的 Python 代码包含了一个神经网络类的实现,应用了我们在上一章中学习到的知识:
Pythonimport numpy as np from scipy.stats import truncnorm def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm( (low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) class NeuralNetwork: def __init__(self, no_of_in_nodes, no_of_out_nodes, no_of_hidden_nodes, learning_rate): self.no_of_in_nodes = no_of_in_nodes self.no_of_out_nodes = no_of_out_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.learning_rate = learning_rate self.create_weight_matrices() def create_weight_matrices(self): """ A method to initialize the weight matrices of the neural network""" rad = 1 / np.sqrt(self.no_of_in_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes)) rad = 1 / np.sqrt(self.no_of_hidden_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_hidden_out = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes)) def train(self): pass def run(self): pass
我们无法用这段代码做很多事情,但我们至少可以初始化它。我们还可以看看权重矩阵:
Pythonsimple_network = NeuralNetwork(no_of_in_nodes = 3, no_of_out_nodes = 2, no_of_hidden_nodes = 4, learning_rate = 0.1) print(simple_network.weights_in_hidden) print(simple_network.weights_hidden_out)
输出:
[[-0.3460287 -0.19427278 -0.19102916] [ 0.56743476 -0.47164202 -0.06910573] [ 0.53013469 -0.05117752 -0.430623 ] [ 0.48414483 0.31263278 -0.08123676]] [[-0.12645547 0.05260599 -0.36278102 -0.32649173] [-0.20841352 -0.01456191 -0.13778649 -0.08920465]]
激活函数、Sigmoid 和 ReLU
在我们可以编写
run
方法之前,我们必须处理激活函数。在神经网络的介绍章节中,我们有以下图示:感知器的输入值由求和函数处理,然后由激活函数转换,将求和函数的输出转换为所需且更合适的输出。求和函数意味着我们将对权重向量和输入值进行矩阵乘法。
神经网络中使用了许多不同的激活函数。关于可能的激活函数最全面的概述之一可以在维基百科上找到。
Sigmoid 函数是常用激活函数之一。我们正在使用的 Sigmoid 函数也称为逻辑函数。
它被定义为:
让我们看看 Sigmoid 函数的图。我们使用 Matplotlib 绘制 Sigmoid 函数:
Pythonimport numpy as np import matplotlib.pyplot as plt def sigma(x): return 1 / (1 + np.exp(-x)) X = np.linspace(-5, 5, 100) plt.plot(X, sigma(X),'b') plt.xlabel('X Axis') plt.ylabel('Y Axis') plt.title('Sigmoid Function') plt.grid() plt.text(2.3, 0.84, r'$\sigma(x)=\frac{1}{1+e^{-x}}$', fontsize=16) plt.show()
观察图表,我们可以看到 Sigmoid 函数将给定的数字 x 映射到 0 到 1 之间的数字范围。不包括 0 和 1!随着 x 值变大,Sigmoid 函数的值越来越接近 1;随着 x 值变小,Sigmoid 函数的值越来越接近 0。
除了我们自己定义 Sigmoid 函数外,我们还可以使用
scipy.special
中的expit
函数,它是 Sigmoid 函数的一种实现。它可以应用于各种数据类型,如int
、float
、list
、numpy.ndarray
等。结果是一个与输入数据 x 形状相同的ndarray
。Pythonfrom scipy.special import expit print(expit(3.4)) print(expit([3, 4, 1])) print(expit(np.array([0.8, 2.3, 8])))
输出:
0.9677045353015494 [0.95257413 0.98201379 0.73105858] [0.68997448 0.90887704 0.99966465]
逻辑函数在神经网络中经常用于引入非线性并将信号映射到指定范围,即 0 和 1。它也广受欢迎,因为其导数(在反向传播中需要)很简单。
及其导数:
Pythonimport numpy as np import matplotlib.pyplot as plt def sigma(x): return 1 / (1 + np.exp(-x)) X = np.linspace(-5, 5, 100) plt.plot(X, sigma(X)) plt.plot(X, sigma(X) * (1 - sigma(X))) plt.xlabel('X Axis') plt.ylabel('Y Axis') plt.title('Sigmoid Function') plt.grid() plt.text(2.3, 0.84, r'$\sigma(x)=\frac{1}{1+e^{-x}}$', fontsize=16) plt.text(0.3, 0.1, r'$\sigma\'(x) = \sigma(x)(1 - \sigma(x))$', fontsize=16) plt.show()
我们也可以用 NumPy 的装饰器
vectorize
定义我们自己的 Sigmoid 函数:Python@np.vectorize def sigmoid(x): return 1 / (1 + np.e ** -x) #sigmoid = np.vectorize(sigmoid) sigmoid([3, 4, 5])
输出:
array([0.95257413, 0.98201379, 0.99330715])
另一个易于使用的激活函数是 ReLU 函数。ReLU 代表修正线性单元。它也称为斜坡函数。它被定义为其参数的正部分,即 x 之间选择最大值。而 Sigmoid 函数需要执行昂贵的指数运算。
。这“目前是最成功和广泛使用的激活函数是修正线性单元(ReLU)”[^1]。ReLU 函数比 Sigmoid 类函数在计算上更高效,因为 ReLU 只需在 0 和参数Python# alternative activation function def ReLU(x): return np.maximum(0.0, x) # derivation of relu def ReLU_derivation(x): if x <= 0: return 0 else: return 1
Pythonimport numpy as np import matplotlib.pyplot as plt X = np.linspace(-5, 6, 100) plt.plot(X, ReLU(X),'b') plt.xlabel('X Axis') plt.ylabel('Y Axis') plt.title('ReLU Function') plt.grid() plt.text(0.8, 0.4, r'$ReLU(x)=max(0, x)$', fontsize=14) plt.show()
添加
run
方法我们现在已经准备好实现神经网络类的
run
(或predict
)方法。我们将使用scipy.special
作为激活函数并将其重命名为activation_function
:Pythonfrom scipy.special import expit as activation_function
run
方法中我们要做的所有事情包括以下几点:-
输入向量与
weights_in_hidden
矩阵的矩阵乘法。 -
对步骤 1 的结果应用激活函数。
-
步骤 2 的结果向量与
weights_hidden_out
矩阵的矩阵乘法。 -
为了得到最终结果:对步骤 3 的结果应用激活函数。
Pythonimport numpy as np from scipy.special import expit as activation_function from scipy.stats import truncnorm def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm( (low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) class NeuralNetwork: def __init__(self, no_of_in_nodes, no_of_out_nodes, no_of_hidden_nodes, learning_rate): self.no_of_in_nodes = no_of_in_nodes self.no_of_out_nodes = no_of_out_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.learning_rate = learning_rate self.create_weight_matrices() def create_weight_matrices(self): """ 一个用于初始化神经网络权重矩阵的方法 """ rad = 1 / np.sqrt(self.no_of_in_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes)) rad = 1 / np.sqrt(self.no_of_hidden_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_hidden_out = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes)) def train(self, input_vector, target_vector): pass def run(self, input_vector): """ 使用输入向量 'input_vector' 运行网络。 'input_vector' 可以是元组、列表或 ndarray。 """ # 将输入向量转换为列向量 input_vector = np.array(input_vector, ndmin=2).T # 计算隐藏层输入并应用激活函数 input_hidden = activation_function(self.weights_in_hidden @ input_vector) # 计算输出层输入并应用激活函数 output_vector = activation_function(self.weights_hidden_out @ input_hidden) return output_vector
我们可以实例化这个类,它将是一个神经网络。在下面的示例中,我们创建一个具有两个输入节点、四个隐藏节点和两个输出节点的网络。
Pythonsimple_network = NeuralNetwork(no_of_in_nodes=2, no_of_out_nodes=2, no_of_hidden_nodes=4, learning_rate=0.6)
我们可以将
run
方法应用于所有形状为(2,)
的数组,以及包含两个数字元素的列表和元组。函数调用的结果由权重的随机值决定:Pythonsimple_network.run([(3, 4)])
输出:
array([[0.54558831], [0.6834667 ]])
注脚
[^1]:
Ramachandran, Prajit; Barret, Zoph; Quoc, V. Le (October 16, 2017). "Searching for Activation Functions".
A NEURAL NETWORK CLASS
We learned in the previous chapter of our tutorial on neural
networks the most important facts about weights. We saw how
they are used and how we can implement them in Python. We
saw that the multiplication of the weights with the input values
can be accomplished with arrays from Numpy by applying
matrix multiplication.
However, what we hadn't done was to test them in a real neural
network environment. We have to create this environment first.
We will now create a class in Python, implementing a neural
network. We will proceed in small steps so that everything is
easy to understand.
The most essential methods our class needs are:
•••__init__ to initialize a class, i.e. we will set
the number of neurons for every layer and
initialize the weight matrices.
run : A method which is applied to a sample,
which which we want to classify. It applies this
sample to the neural network. We could say, we
'run' the network to 'predict' the result. This
method is in other implementations often known
as predict .
train : This method gets a sample and the corresponding target value as an input. With this
input it can adjust the weight values if necessary. This means the network learns from an input.
Seen from the user point of view, we 'train' the network. In sklearn for example, this method
is called fit
We will postpone the definition of the train and run method until later. The weight matrices should be
initialized inside of the __init__ method. We do this indirectly. We define a method
create_weight_matrices and call it in __init__ . In this way, the init method remains clear.
We will also postpone adding bias nodes to the layers.
153
The following Python code contains an implementation of a neural network class applying the knowledge we
worked out in the previous chapter:
import numpy as np
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm(
(low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd)
class NeuralNetwork:
def __init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_out_nodes = no_of_out_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.learning_rate = learning_rate
self.create_weight_matrices()
def create_weight_matrices(self):
rad = 1 / np.sqrt(self.no_of_in_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes))
rad = 1 / np.sqrt(self.no_of_hidden_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_hidden_out = X.rvs((self.no_of_out_nodes,
self.no_of_hidden_nodes))
deftrain(self):
pass
defrun(self):
pass
We cannot do a lot with this code, but we can at least initialize it. We can also have a look at the weight
matrices:
simple_network = NeuralNetwork(no_of_in_nodes = 3,
154
no_of_out_nodes = 2,
no_of_hidden_nodes = 4,
learning_rate = 0.1)
print(simple_network.weights_in_hidden)
print(simple_network.weights_hidden_out)
[[-0.3460287 -0.19427278 -0.19102916]
[ 0.56743476 -0.47164202 -0.06910573]
[ 0.53013469 -0.05117752 -0.430623 ]
[ 0.48414483 0.31263278 -0.08123676]]
[[-0.12645547 0.05260599 -0.36278102 -0.32649173]
[-0.20841352 -0.01456191 -0.13778649 -0.08920465]]
ACTIVATION FUNCTIONS, SIGMOID AND RELU
Before we can program the run method, we have to deal with the activation function. We had the following
diagram in the introductory chapter on neural networks:
The input values of a perceptron are processed by the summation function and followed by an activation
function, transforming the output of the summation function into a desired and more suitable output. The
summation function means that we will have a matrix multiplication of the weight vectors and the input
values.
There are lots of different activation functions used in neural networks. One of the most comprehensive
overviews of possible activation functions can be found at Wikipedia.
The sigmoid function is one of the often used activation functions. The sigmoid function, which we are using,
is also known as the Logistic function.
It is defined as
1
σ(x) =
1 + e − x
Let us have a look at the graph of the sigmoid function. We use matplotlib to plot the sigmoid function:
import numpy as np
155
import matplotlib.pyplot as plt
def sigma(x):
return 1 / (1 + np.exp(-x))
X = np.linspace(-5, 5, 100)
plt.plot(X, sigma(X),'b')
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.title('Sigmoid Function')
plt.grid()
plt.text(2.3, 0.84, r'$\sigma(x)=\frac{1}{1+e^{-x}}$', fontsize=1
6)
plt.show()
Looking at the graph, we can see that the sigmoid function maps a given number x into the range of numbers
between 0 and 1. 0 and 1 not included! As the value of x gets larger, the value of the sigmoid function gets
closer and closer to 1 and as x gets smaller, the value of the sigmoid function is approaching 0.
Instead of defining the sigmoid function ourselves, we can also use the expit function from
scipy.special , which is an implementation of the sigmoid function. It can be applied on various data
classes like int, float, list, numpy,ndarray and so on. The result is an ndarray of the same shape as the input
data x.
156
from scipy.special import expit
print(expit(3.4))
print(expit([3, 4, 1]))
print(expit(np.array([0.8, 2.3, 8])))
0.9677045353015494
[0.95257413 0.98201379 0.73105858]
[0.68997448 0.90887704 0.99966465]
The logistic function is often often used in neural networks to introduce nonlinearity in the model and to map
signals into a specified range, i.e. 0 and 1. It is also well liked because the derivative - needed in
backpropagation - is simple.
1
σ(x) =
1 + e − x
and its derivative:
σ ′ (x) = σ(x)(1 − σ(x))
import numpy as np
import matplotlib.pyplot as plt
def sigma(x):
return 1 / (1 + np.exp(-x))
X = np.linspace(-5, 5, 100)
plt.plot(X, sigma(X))
plt.plot(X, sigma(X) * (1 - sigma(X)))
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.title('Sigmoid Function')
plt.grid()
plt.text(2.3, 0.84, r'$\sigma(x)=\frac{1}{1+e^{-x}}$', fontsize=1
6)
plt.text(0.3, 0.1, r'$\sigma\'(x) = \sigma(x)(1 - \sigma(x))$', fo
ntsize=16)
plt.show()
157
We can also define our own sigmoid function with the decorator vectorize from numpy:
@np.vectorize
def sigmoid(x):
return 1 / (1 + np.e ** -x)
#sigmoid = np.vectorize(sigmoid)
sigmoid([3, 4, 5])
Output:array([0.95257413, 0.98201379, 0.99330715])
Another easy to use activation function is the ReLU function. ReLU stands for rectified linear unit. It is also
known as the ramp function. It is defined as the positve part of its argument, i.e. y = max (0, x). This is
"currently, the most successful and widely-used activation function is the Rectified Linear Unit (ReLU)" 1 The
ReLu function is computationally more efficient than Sigmoid like functions, because Relu means only
choosing the maximum between 0 and the argument x . Whereas Sigmoids need to perform expensive
exponential operations.
# alternative activation function
def ReLU(x):
return np.maximum(0.0, x)
# derivation of relu
def ReLU_derivation(x):
if x <= 0:
return 0
else:
return 1
158
import numpy as np
import matplotlib.pyplot as plt
X = np.linspace(-5, 6, 100)
plt.plot(X, ReLU(X),'b')
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.title('ReLU Function')
plt.grid()
plt.text(0.8, 0.4, r'$ReLU(x)=max(0, x)$', fontsize=14)
plt.show()
ADDING A RUN METHOD
We have everything together now to implement the run (or predict ) method of our neural network
class. We will use scipy.special as the activation function and rename it to
activation_function :
from scipy.special import expit as activation_function
All we have to do in the run method consists of the following.
1.
2.
3.
4.
Matrix multiplication of the input vector and the weights_in_hidden matrix.
Applying the activation function to the result of step 1
Matrix multiplication of the result vector of step 2 and the weights_in_hidden matrix.
To get the final result: Applying the activation function to the result of 3
import numpy as np
from scipy.special import expit as activation_function
159
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm(
(low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd)
class NeuralNetwork:
def __init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_out_nodes = no_of_out_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.learning_rate = learning_rate
self.create_weight_matrices()
def create_weight_matrices(self):
""" A method to initialize the weight matrices of the neur
al network"""
rad = 1 / np.sqrt(self.no_of_in_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes))
rad = 1 / np.sqrt(self.no_of_hidden_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_hidden_out = X.rvs((self.no_of_out_nodes,
self.no_of_hidden_nodes))
def train(self, input_vector, target_vector):
pass
def run(self, input_vector):
"""
running the network with an input vector 'input_vector'.
'input_vector' can be tuple, list or ndarray
"""
# turning the input vector into a column vector
input_vector = np.array(input_vector, ndmin=2).T
input_hidden = activation_function(self.weights_in_hidden
160
@ input_vector)
output_vector = activation_function(self.weights_hidden_ou
t @ input_hidden)
return output_vector
We can instantiate an instance of this class, which will be a neural network. In the following example we
create a network with two input nodes, four hidden nodes, and two output nodes.
simple_network = NeuralNetwork(no_of_in_nodes=2,
no_of_out_nodes=2,
no_of_hidden_nodes=4,
learning_rate=0.6)
We can apply the run method to all arrays with a shape of (2,), also lists and tuples with two numerical
elements. The result of the call is defined by the random values of the weights:
simple_network.run([(3, 4)])
Output:array([[0.54558831],
[0.6834667 ]])
FOOTNOTES
1
Ramachandran, Prajit; Barret, Zoph; Quoc, V. Le (October 16, 2017). "Searching for Activation Functions". -
-
引言
我们已经在之前的 Python 神经网络教程章节中介绍过。我们“运行神经网络”一章中的网络缺乏学习能力。它们只能在随机设置的权重值下运行。因此,我们无法用它们解决任何分类问题。然而,“简单神经网络”一章中的网络具有学习能力,但我们只将线性网络用于线性可分的类别。
当然,我们希望编写能够学习的通用 ANN(人工神经网络)。为此,我们必须理解反向传播 (backpropagation)。反向传播是一种常用的训练人工神经网络,特别是深度神经网络的方法。
反向传播是计算梯度 (gradient) 所必需的,我们需要梯度来调整权重矩阵的权重。我们网络中神经元(节点)的权重通过计算损失函数的梯度进行调整。为此,使用了梯度下降优化算法。它也称为误差反向传播。
人们常常被其中使用的数学吓退。我们试图用简单的术语来解释它。
许多文章或教程都以山来解释梯度下降。想象一下,你在夜晚或浓雾中被直升机放在一座山上,不一定是山顶。让我们进一步想象这座山在一个岛上,你想达到海平面。你必须下山,但你几乎什么也看不见,也许只有几米远。你的任务是找到下山的路,但你看不见小径。你可以使用梯度下降法。这意味着你正在检查你当前位置的陡峭程度。你将沿着最陡峭的下降方向前进。你只走几步,然后再次停下来重新定位。这意味着你再次应用前面描述的过程,即你正在寻找最陡峭的下降。
这个过程在下面的二维图中描绘。
这样继续下去,你将到达一个没有进一步下降的位置。
每个方向都向上。你可能已经达到了最深层——全局最小值,但你也可能被困在局部最小值中。如果你从我们图片右侧的位置开始,一切都很好,但从左侧开始,你将陷入局部最小值。
反向传播详解
现在,我们必须深入细节,即数学部分。
我们将从更简单的情况开始。我们来看一个线性网络。线性神经网络是指输出信号通过对所有加权输入信号求和而创建的网络。不会对这个和应用任何激活函数,这就是线性化的原因。
我们将使用以下简单的网络。
当我们训练网络时,我们有样本和相应的标签。对于每个输出值 \(o_i\),我们都有一个标签 \(t_i\),它是目标或期望值。如果标签等于输出,则结果正确,神经网络没有出错。原则上,误差是目标与实际输出之间的差异:
我们稍后将使用平方误差函数,因为它对算法具有更好的特性:
\[ e_i = \frac{(t_i - o_i)^2}{2} \]
我们想通过以下带有值的示例来阐明误差如何反向传播:
我们将看一下输出值 \(o_1\),它取决于值 \(w_11\),\(w_12\),\(w_13\)和 \(w_14\)。假设计算值 (\(o_1\)) 是 0.92,期望值 (\(t_1\)) 是 1。在这种情况下,误差是:
\[ e_1 = t_1 - o_1 = 1 - 0.92 = 0.08 \]
误差 \(e_2\) 可以这样计算:
\[ e_2 = t_2 - o_2 = 1 - 0.18 = 0.82 \]
根据这个误差,我们必须相应地改变传入值的权重。我们有四个权重,所以我们可以平均分配误差。然而,按比例分配更合理,即根据权重值进行分配。一个权重相对于其他权重越大,它对误差的责任就越大。这意味着我们可以将 \(w_{11}\) 中误差 \(e_1\) 的分数计算为:
\( e_1 \cdot \frac{w_{11}}{\sum_{i=1}^{4} w_{1i}} \)
这意味着在我们的示例中:
\(0.08 \cdot \frac{0.6}{ 0.6 + 0.1 + 0.15 + 0.25} = 0.0343 \)
我们的隐藏层和输出层之间的权重矩阵——我们在上一章中称之为 'who' ——中的总误差如下所示:
你可以看到左矩阵中的分母总是相同的。它起着缩放因子的作用。我们可以去掉它,这样计算会简单得多:
如果你将右侧的矩阵与我们“使用 Python 和 Numpy 的神经网络”一章中的 'who' 矩阵进行比较,你会发现它是 'who' 的转置。
\(e_{who}= {who.T} \cdot e\)
所以,这是线性神经网络的简单部分。我们到现在还没有考虑激活函数。
我们想在一个带有激活函数(即非线性网络)的网络中计算误差。误差函数的导数描述了斜率。正如我们在本章开头提到的,我们想要下降。导数描述了当权重 \(w_{kj}\)改变时,误差 E 如何变化:
\( \frac{\partial E}{\partial w_{kj}} \)
6666所有输出节点 \(o_i\)( ) 上的误差函数 E,其中 n 是输出节点的总数:
现在,我们可以将其插入到我们的导数中:
如果你看一下我们的示例网络,你会发现一个输出节点 \(o_k\)只取决于由权重 \(w_{ki}\)(其中 ,m 是隐藏节点的数量)创建的输入信号。
下图进一步阐明了这一点:
这意味着我们可以独立地计算每个输出节点的误差。这意味着我们可以从我们的求和中删除所有 k 的误差看起来简单得多:
(其中 )的表达式。因此,现在计算节点目标值 \(t_k\) 是一个常数,因为它不依赖于任何输入信号或权重。我们可以应用链式法则来对前面的项进行微分以简化事情:
在我们教程的上一章中,我们使用 Sigmoid 函数作为激活函数:
输出节点 \(o_k\)是通过对加权输入信号之和应用 Sigmoid 函数来计算的。这意味着我们可以通过用这个函数替换 \(o_k\)来进一步转换我们的导数项:
其中 m 是隐藏节点的数量。
Sigmoid 函数很容易求导:
现在完整的微分看起来像这样:
最后一部分必须对 \(w_{kj}\) 求导。这意味着所有乘积的导数都将为 0,除了项 \(w_{kj}\)\(h_j\),它对 \(w_{kj}\)的导数为 \(h_j\):
这就是我们需要在下一章中实现
NeuralNetwork
类的train
方法所需的一切。
INTRODUCTION
We already wrote in the previous chapters of our
tutorial on Neural Networks in Python. The networks
from our chapter Running Neural Networks lack the
capabilty of learning. They can only be run with
randomly set weight values. So we cannot solve any
classification problems with them. However, the
networks in Chapter Simple Neural Networks were
capable of learning, but we only used linear networks
for linearly separable classes.
Of course, we want to write general ANNs, which are
capable of learning. To do so, we will have to
understand backpropagation. Backpropagation is a
commonly used method for training artificial neural
networks, especially deep neural networks.
Backpropagation is needed to calculate the gradient,
which we need to adapt the weights of the weight matrices. The weight of the neuron (nodes) of our network
are adjusted by calculating the gradient of the loss function. For this purpose a gradient descent optimization
algorithm is used. It is also called backward propagation of errors.
Quite often people are frightened away by the mathematics used in it. We try to explain it in simple terms.
Explaining gradient descent starts in many articles or tutorials with mountains. Imagine you are put on a
mountain, not necessarily the top, by a helicopter at night or heavy fog. Let's further imagine that this
mountain is on an island and you want to reach sea level. You have to go down, but you hardly see anything,
maybe just a few metres. Your task is to find your way down, but you cannot see the path. You can use the
method of gradient descent. This means that you are examining the steepness at your current position. You
will proceed in the direction with the steepest descent. You take only a few steps and then you stop again to
reorientate yourself. This means you are applying again the previously described procedure, i.e. you are
looking for the steepest descend.
This procedure is depicted in the following diagram in a two-dimensional space.
162
Going on like this you will arrive at a position, where there is no further descend.
Each direction goes upwards. You may have reached the deepest level - the global minimum -, but you might
as well be stuck in a basin. If you start at the position on the right side of our image, everything works out fine,
but from the leftside, you will be stuck in a local minimum.
BACKPROPAGATION IN DETAIL
Now, we have to go into the details, i.e. the mathematics.
We will start with the simpler case. We look at a linear network. Linear neural networks are networks where
the output signal is created by summing up all the weighted input signals. No activation function will be
applied to this sum, which is the reason for the linearity.
The will use the following simple network.
When we are training the network we have samples and corresponding labels. For each output value o we
ihave a label t i, which is the target or the desired value. If the label is equal to the output, the result is correct
163
and the neural network has not made an error. Principially, the error is the difference between the target and
the actual output:
e i = t i − o i
We will later use a squared error function, because it has better characteristics for the algorithm:
1
e i =
(ti
− o i) 2
2We want to clarify how the error backpropagates with the following example with values:
We will have a look at the output value o 1, which is depending on the values w 11, w 12, w 13 and w 14. Let's
assume the calculated value (o 1) is 0.92 and the desired value (t 1) is 1. In this case the error is
e = t − o = 1 − 0.92 = 0.08
1 1 1The eror e 2 can be calculated like this:
e = t − o = 1 − 0.18 = 0.82
2 2 2164
Depending on this error, we have to change the weights from the incoming values accordingly. We have four
weights, so we could spread the error evenly. Yet, it makes more sense to to do it proportionally, according to
the weight values. The larger a weight is in relation to the other weights, the more it is responsible for the
error. This means that we can calculate the fraction of the error e 1 in w 11 as:
w11
e 1 ⋅
∑ 4 w 1i
i =1
This means in our example:
0.6
0.08 ⋅
= 0.0343
0.6 + 0.1 + 0.15 + 0.25
The total error in our weight matrix between the hidden and the output layer - we called it in our previous
chapter 'who' - looks like this
165
e who =
[ ∑ ∑ ∑ ∑ i 4 i 4 4 i i 4 w w w w =1
=1
=1
=1
13
12
11
14
w w w w 1i
1i
1i
1i
∑ ∑ ∑ ∑ i 4 i 4 i i 4 4 w24
w23
w21
w22
=1
=1
=1
=1
w w w w 2i
2i
2i
2i
∑ ∑ ∑ ∑ 4 4 i i i 4 i 4 w w w w =1
=1
=1
=1
31
32
34
33
w w w w 3i
3i
3i
3i
]
⋅
[ e3
e2
e1
]
You can see that the denominator in the left matrix is always the same. It functions like a scaling factor. We
can drop it so that the calculation gets a lot simpler:
e who =
[ w w w w 14
12
11
13
w w w w 22
23
24
21
w w w w 33
34
32
31
] ⋅
[ e e e 2
3
1
]
If you compare the matrix on the right side with the 'who' matrix of our chapter Neuronal Network Using
Python and Numpy, you will notice that it is the transpose of 'who'.
e who = who. T ⋅ e
So, this has been the easy part for linear neural networks. We haven't taken into account the activation function
until now.
We want to calculate the error in a network with an activation function, i.e. a non-linear network. The
derivation of the error function describes the slope. As we mentioned in the beginning of the this chapter, we
want to descend. The derivation describes how the error E changes as the weight w kj changes:
166
∂E
∂wkj
The error function E over all the output nodes o i (i = 1, . . . n) where n is the total number of output nodes:
n
1
E = ∑ (t i − o i) 2
2
i =1
Now, we can insert this in our derivation:
n
∂E
∂ 1
=
∑ (t i
− o i)2
∂wkj
∂w kj 2 i = 1If you have a look at our example network, you will see that an output node o only depends on the input
ksignals created with the weights w ki with i = 1, ...m and m the number of hidden nodes.
The following diagram further illuminates this:
This means that we can calculate the error for every output node independently of each other. This means that
we can remove all expressions ti − oi with i ≠ k from our summation. So the calculation of the error for a node
k looks a lot simpler now:
∂E
∂ 1
=
(t k
− o k) 2
∂w ∂w 2kj
kjThe target value t k is a constant, because it is not depending on any input signals or weights. We can apply the
chain rule for the differentiation of the previous term to simplify things:
167
∂E
∂E
∂o k
=
⋅
∂w kj
∂o k
∂w kj
In the previous chapter of our tutorial, we used the sigmoid function as the activation function:
1
σ(x) =
1 + e − x
The output node o k is calculated by applying the sigmoid function to the sum of the weighted input signals.
This means that we can further transform our derivative term by replacing o by this function:
km
∂E
∂
= (tk − ok) ⋅
σ( ∑ w kih i
)
∂w kj
∂w kj i = 1where m is the number of hidden nodes.
The sigmoid function is easy to differentiate:
∂σ(x)
= σ(x) ⋅ (1 − σ(x))
∂x
The complete differentiation looks like this now:
m
m
m
∂E
∂
= (t k − o k) ⋅ σ( ∑ w kih i) ⋅ (1 − σ( ∑ w kih i))
∑ w kih
i
∂w ∂wkj
i =1
i =1
kj i = 1The last part has to be differentiated with respect to w kj. This means that the derivation of all the products will
be 0 except the the term w kjh j) which has the derivative h j with respect to w kj:
m
m
∂E
= (tk − ok) ⋅ σ( ∑ w kih i) ⋅ (1 − σ( ∑ wkihi)) ⋅ h j
∂wkj
i =1
i =1
This is what we need to implement the method 'train' of our NeuralNetwork class in the following chapter.
In [ ]: -
引言
在“运行神经网络”章节中,我们用 Python 编写了一个名为
NeuralNetwork
的类。这个类的实例是三层网络。当我们实例化一个这种类型的人工神经网络 (ANN) 时,层之间的权重矩阵是自动随机选择的。甚至有可能对这样的 ANN 进行一些输入运行,但除了测试目的之外,这并没有多大意义。这样的 ANN 无法提供正确的分类结果。事实上,分类结果与预期结果毫不相关。权重矩阵的值必须根据分类任务进行设置。我们需要改进权重值,这意味着我们必须训练我们的网络。为了训练它,我们必须在
train
方法中实现反向传播。如果您不理解反向传播并希望理解它,我们建议您回到“神经网络中的反向传播”一章。在了解并希望理解反向传播之后,您就可以完全理解
train
方法了。train
方法以输入向量和目标向量作为参数被调用。向量的形状可以是一维的,但它们将自动转换为正确的二维形状,即reshape(input_vector.size, 1)
和reshape(target_vector.size, 1)
。在此之后,我们调用run
方法来获取网络输出output_vector_network = self.run(input_vector)
。这个输出可能与target_vector
不同。我们通过从target_vector
中减去网络输出output_vector_network
来计算输出误差output_error
。Pythonimport numpy as np from scipy.special import expit as activation_function from scipy.stats import truncnorm def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm( (low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) class NeuralNetwork: def __init__(self, no_of_in_nodes, no_of_out_nodes, no_of_hidden_nodes, learning_rate): self.no_of_in_nodes = no_of_in_nodes self.no_of_out_nodes = no_of_out_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.learning_rate = learning_rate self.create_weight_matrices() def create_weight_matrices(self): """ 一个初始化神经网络权重矩阵的方法 """ rad = 1 / np.sqrt(self.no_of_in_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes)) rad = 1 / np.sqrt(self.no_of_hidden_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_hidden_out = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes)) def train(self, input_vector, target_vector): """ input_vector 和 target_vector 可以是元组、列表或 ndarray """ # 确保向量具有正确的形状 input_vector = np.array(input_vector) input_vector = input_vector.reshape(input_vector.size, 1) target_vector = np.array(target_vector).reshape(target_vector.size, 1) # 前向传播 output_vector_hidden = activation_function(self.weights_in_hidden @ input_vector) output_vector_network = activation_function(self.weights_hidden_out @ output_vector_hidden) # 计算输出误差 output_error = target_vector - output_vector_network # 更新隐藏层到输出层的权重 tmp = output_error * output_vector_network * (1.0 - output_vector_network) self.weights_hidden_out += self.learning_rate * (tmp @ output_vector_hidden.T) # 计算隐藏层误差: hidden_errors = self.weights_hidden_out.T @ output_error # 更新输入层到隐藏层的权重: tmp = hidden_errors * output_vector_hidden * (1.0 - output_vector_hidden) self.weights_in_hidden += self.learning_rate * (tmp @ input_vector.T) def run(self, input_vector): """ 使用输入向量 'input_vector' 运行网络。 'input_vector' 可以是元组、列表或 ndarray """ # 确保 input_vector 是一个列向量: input_vector = np.array(input_vector) input_vector = input_vector.reshape(input_vector.size, 1) input4hidden = activation_function(self.weights_in_hidden @ input_vector) output_vector_network = activation_function(self.weights_hidden_out @ input4hidden) return output_vector_network def evaluate(self, data, labels): """ 计算实际结果与目标结果对应的次数。 如果最大值的索引与独热表示中“1”的索引对应,则认为结果正确, 例如: res = [0.1, 0.132, 0.875] labels[i] = [0, 0, 1] """ corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() if res_max == labels[i].argmax(): corrects += 1 else: wrongs += 1 return corrects, wrongs
我们假设您将上述代码保存到名为
neural_networks1.py
的文件中。在接下来的示例中,我们将使用这个名称。要测试这个神经网络类,我们需要训练和测试数据。我们使用
sklearn.datasets
中的make_blobs
来创建数据。Pythonfrom sklearn.datasets import make_blobs n_samples = 500 blob_centers = ([2, 6], [6, 2], [7, 7]) n_classes = len(blob_centers) data, labels = make_blobs(n_samples=n_samples, centers=blob_centers, random_state=7)
让我们可视化之前创建的数据:
Pythonimport matplotlib.pyplot as plt colours = ('green', 'red', "yellow") fig, ax = plt.subplots() for n_class in range(n_classes): ax.scatter(data[labels==n_class][:, 0], data[labels==n_class][:, 1], c=colours[n_class], s=40, label=str(n_class)) plt.show() # 添加这一行来显示图表
标签表示不正确。它们是一个一维向量:
Pythonlabels[:7]
输出:
array([2, 2, 1, 0, 2, 0, 1])
我们需要每个标签的独热 (one-hot) 表示。因此标签表示为:
标签
独热表示
0
(1, 0, 0)
1
(0, 1, 0)
2
(0, 0, 1)
我们可以使用以下命令轻松更改标签:
Pythonimport numpy as np labels = np.arange(n_classes) == labels.reshape(labels.size, 1) labels = labels.astype(np.float64) # 将np.float改为np.float64,以避免未来版本警告 labels[:7]
输出:
array([[0., 0., 1.], [0., 0., 1.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.], [1., 0., 0.], [0., 1., 0.]])
现在我们准备创建训练集和测试集:
Pythonfrom sklearn.model_selection import train_test_split res = train_test_split(data, labels, train_size=0.8, test_size=0.2, random_state=42) train_data, test_data, train_labels, test_labels = res train_labels[:10]
输出:
array([[0., 0., 1.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.], [0., 0., 1.], [1., 0., 0.], [0., 1., 0.], [1., 0., 0.], [1., 0., 0.], [0., 0., 1.]])
我们创建一个具有两个输入节点和三个输出节点(每个类别一个输出节点)的神经网络:
Pythonfrom neural_networks1 import NeuralNetwork # 确保 neural_networks1.py 在当前路径下 simple_network = NeuralNetwork(no_of_in_nodes=2, no_of_out_nodes=3, no_of_hidden_nodes=5, learning_rate=0.3)
下一步是使用我们的训练样本中的数据和标签训练网络:
Pythonfor i in range(len(train_data)): simple_network.train(train_data[i], train_labels[i])
现在我们必须检查我们的网络学习得如何。为此,我们将使用
evaluate
函数:Pythonsimple_network.evaluate(train_data, train_labels)
输出:
(390, 10)
(这表示 390 个正确分类和 10 个错误分类)
带有偏置节点的神经网络
我们已经在“简单神经网络”章节中介绍了偏置节点的基本思想和必要性,其中我们重点关注了非常简单的线性可分数据集。我们了解到,偏置节点是始终返回相同输出的节点。换句话说:它是一个不依赖于某些输入并且没有任何输入的节点。偏置节点的值通常设置为 1,但也可以设置为其他值。除了零,这显然没有意义。如果神经网络在给定层中没有偏置节点,当特征值为 0 时,它将无法在下一层中产生与 0 不同的输出。一般来说,我们可以说偏置节点用于增加网络的灵活性以适应数据。通常,每层不会超过一个偏置节点。唯一的例外是输出层,因为向该层添加偏置节点没有意义。
下图显示了我们之前使用的三层神经网络的前两层:
我们可以从这个图表中看到,我们的权重矩阵需要额外一列,并且偏置值必须添加到输入向量中:
同样,隐藏层和输出层之间的权重矩阵情况也类似:
相应矩阵也是如此:
以下是一个完整的 Python 类,实现了带有偏置节点的网络:
Pythonimport numpy as np from scipy.stats import truncnorm from scipy.special import expit as activation_function def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm( (low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) class NeuralNetwork: def __init__(self, no_of_in_nodes, no_of_out_nodes, no_of_hidden_nodes, learning_rate, bias=None): self.no_of_in_nodes = no_of_in_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.no_of_out_nodes = no_of_out_nodes self.learning_rate = learning_rate self.bias = bias self.create_weight_matrices() def create_weight_matrices(self): """ 一个初始化带有可选偏置节点的神经网络权重矩阵的方法 """ bias_node = 1 if self.bias else 0 rad = 1 / np.sqrt(self.no_of_in_nodes + bias_node) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes + bias_node)) rad = 1 / np.sqrt(self.no_of_hidden_nodes + bias_node) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_hidden_out = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes + bias_node)) def train(self, input_vector, target_vector): """ input_vector 和 target_vector 可以是元组、列表或 ndarray """ # 确保向量具有正确的形状 input_vector = np.array(input_vector) input_vector = input_vector.reshape(input_vector.size, 1) if self.bias: # 在 input_vector 末尾添加偏置节点 input_vector = np.concatenate((input_vector, [[self.bias]])) target_vector = np.array(target_vector).reshape(target_vector.size, 1) # 前向传播 output_vector_hidden = activation_function(self.weights_in_hidden @ input_vector) if self.bias: output_vector_hidden = np.concatenate((output_vector_hidden, [[self.bias]])) output_vector_network = activation_function(self.weights_hidden_out @ output_vector_hidden) # 计算输出误差 output_error = target_vector - output_vector_network # 更新隐藏层到输出层的权重: tmp = output_error * output_vector_network * (1.0 - output_vector_network) self.weights_hidden_out += self.learning_rate * (tmp @ output_vector_hidden.T) # 计算隐藏层误差: hidden_errors = self.weights_hidden_out.T @ output_error # 更新输入层到隐藏层的权重: tmp = hidden_errors * output_vector_hidden * (1.0 - output_vector_hidden) if self.bias: x = (tmp @ input_vector.T)[:-1, :] # 截断最后一行 (偏置节点的导数) else: x = tmp @ input_vector.T self.weights_in_hidden += self.learning_rate * x def run(self, input_vector): """ 使用输入向量 'input_vector' 运行网络。 'input_vector' 可以是元组、列表或 ndarray """ # 确保 input_vector 是一个列向量: input_vector = np.array(input_vector) input_vector = input_vector.reshape(input_vector.size, 1) if self.bias: # 在 input_vector 末尾添加偏置节点 input_vector = np.concatenate((input_vector, [[1]])) input4hidden = activation_function(self.weights_in_hidden @ input_vector) if self.bias: input4hidden = np.concatenate((input4hidden, [[1]])) output_vector_network = activation_function(self.weights_hidden_out @ input4hidden) return output_vector_network def evaluate(self, data, labels): corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() if res_max == labels[i].argmax(): corrects += 1 else: wrongs += 1 return corrects, wrongs
我们可以再次使用我们之前创建的类来测试我们的分类器:
Pythonfrom neural_networks2 import NeuralNetwork # 确保 neural_networks2.py 在当前路径下 simple_network = NeuralNetwork(no_of_in_nodes=2, no_of_out_nodes=3, no_of_hidden_nodes=5, learning_rate=0.1, bias=1) # 启用偏置节点 for i in range(len(train_data)): simple_network.train(train_data[i], train_labels[i]) simple_network.evaluate(train_data, train_labels)
输出:
(382, 18)
(这表示 382 个正确分类和 18 个错误分类)
练习
我们在“数据创建”章节中在
data
文件夹中创建了一个名为strange_flowers.txt
的文件。创建一个神经网络来对这些“花”进行分类:数据如下所示:
0.000,240.000,100.000,3.020 253.000,99.000,13.000,3.875 202.000,107.000,6.000,4.1 186.000,84.000,6.000,4.068 0.000,244.000,103.000,3.386 0.000,246.000,98.000,2.955 241.000,103.000,3.000,4.049 236.000,104.000,12.000,3.087 244.000,109.000,1.000,3.111 253.000,97.000,8.000,3.752 231.000,92.000,1.000,3.488 0.000,250.000,103.000,3.379
解决方案:
Pythonimport numpy as np from sklearn import preprocessing from sklearn.model_selection import train_test_split from neural_networks2 import NeuralNetwork # 假设这个文件包含带有偏置节点的类 # 加载数据 c = np.loadtxt("data/strange_flowers.txt", delimiter=" ") data = c[:, :-1] labels = c[:, -1] # 检查原始数据的前5行 print("原始数据的前5行:") print(data[:5]) print("原始数据的形状:", data.shape) print("原始标签的形状:", labels.shape) Output:array([[242.
, 117.
,
1.
,
3.87],
[236.
, 104.
,
6.
,
4.11],
[238.
, 116.
,
5.
,
3.9 ],
[248.
, 96.
,
6.
,
3.91],
[252.
, 104.
,
4.
,
3.75]]) # 获取类别数量 (标签是最后一列,所以它决定了类别数量) # 修正:根据标签的唯一值来确定类别数量 n_classes = len(np.unique(labels)) # 将标签转换为独热编码 # np.arange(n_classes) 创建 [0, 1, 2, ...] 的数组 # labels.reshape(labels.size, 1) 将标签转换为列向量 # == 操作会进行广播,将每个标签与 np.arange(n_classes) 中的每个元素进行比较 labels_one_hot = (np.arange(n_classes) == labels.reshape(labels.size, 1)).astype(np.float64) print("\n独热编码标签的前3行:") print(labels_one_hot[:3]) print("独热编码标签的形状:", labels_one_hot.shape) Output:array([[0., 1., 0., 0.],
[0., 1., 0., 0.],
[0., 1., 0., 0.]])
# 数据缩放 data = preprocessing.scale(data) print("\n缩放后的数据前5行:") print(data[:5]) print("缩放后数据的形状:", data.shape) Output:(795, 4)
# 划分训练集和测试集 res = train_test_split(data, labels_one_hot, # 使用独热编码的标签 train_size=0.8, test_size=0.2, random_state=42) train_data, test_data, train_labels, test_labels = res print("\n训练标签的前10行:") print(train_labels[:10])
Output:array([[0., 0., 1., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.],
[0., 0., 1., 0.],
[0., 0., 0., 1.],
[0., 0., 1., 0.],
[0., 1., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 0., 1.],
[0., 0., 1., 0.]]) # 创建神经网络实例 # 输入节点数量由数据特征决定,输出节点数量由类别数量决定 simple_network = NeuralNetwork(no_of_in_nodes=data.shape[1], # 根据数据列数设置输入节点 no_of_out_nodes=n_classes, # 根据类别数量设置输出节点 no_of_hidden_nodes=20, # 隐藏节点数量可调整 learning_rate=0.3, bias=1) # 训练神经网络 epochs = 500 # 增加训练轮次以获得更好的性能 for epoch in range(epochs): for i in range(len(train_data)): simple_network.train(train_data[i], train_labels[i]) # 评估训练集上的性能 corrects_train, wrongs_train = simple_network.evaluate(train_data, train_labels) print(f"\n训练集评估: 正确分类 {corrects_train}, 错误分类 {wrongs_train}") print(f"训练集准确率: {corrects_train / (corrects_train + wrongs_train):.4f}") # 评估测试集上的性能 corrects_test, wrongs_test = simple_network.evaluate(test_data, test_labels) print(f"测试集评估: 正确分类 {corrects_test}, 错误分类 {wrongs_test}") print(f"测试集准确率: {corrects_test / (corrects_test + wrongs_test):.4f}")在这个解决方案中,我做了一些调整和补充:
-
明确了
np.float
应更新为np.float64
以适应 NumPy 的未来版本。 -
在可视化
make_blobs
数据时添加了plt.show()
以确保图表显示。 -
在加载
strange_flowers.txt
数据后,动态确定n_classes
,因为原始文本中n_classes
的定义data.shape[1]
是基于输入特征数量,而不是基于标签数量,这可能导致错误。正确的做法是根据标签的唯一值来确定类别数量。 -
为
simple_network
的no_of_in_nodes
和no_of_out_nodes
使用动态值,这使得代码更具通用性,可以适应不同维度的数据集。 -
增加了训练轮次 (epochs) 的概念(设置为 500),因为单次遍历训练数据通常不足以让网络学习。
-
添加了测试集评估,这是机器学习中非常重要的一步,用于衡量模型在新数据上的泛化能力。
INTRODUCTION
In the chapter "Running Neural
Networks", we programmed a class in
Python code called 'NeuralNetwork'. The
instances of this class are networks with
three layers. When we instantiate an ANN
of this class, the weight matrices between
the layers are automatically and randomly
chosen. It is even possible to run such a
ANN on some input, but naturally it
doesn't make a lot of sense exept for
testing purposes. Such an ANN cannot
provide correct classification results. In
fact, the classification results are in no
way adapted to the expected results. The
values of the weight matrices have to be
set according the the classification task.
We need to improve the weight values,
which means that we have to train our network. To train it we have to implement backpropagation in the
train method. If you don't understand backpropagation and want to understand it, we recommend to go
back to the chapter Backpropagation in Neural Networks.
After knowing und hopefully understanding backpropagation, you are ready to fully understand the train
method.
The train method is called with an input vector and a target vector. The shape of the vectors can be one-
dimensional, but they will be automatically turned into the correct two-dimensional shape, i.e.
reshape(input_vector.size, 1) and reshape(target_vector.size, 1) . After this
we call the run method to get the result of the network output_vector_network =
self.run(input_vector) . This output may differ from the target_vector . We calculate the
output_error by subtracting the output of the network output_vector_network from the
target_vector .
import numpy as np
from scipy.special import expit as activation_function
169
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm(
(low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd)
class NeuralNetwork:
def __init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_out_nodes = no_of_out_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.learning_rate = learning_rate
self.create_weight_matrices()
def create_weight_matrices(self):
""" A method to initialize the weight matrices of the neur
al network"""
rad = 1 / np.sqrt(self.no_of_in_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes))
rad = 1 / np.sqrt(self.no_of_hidden_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_hidden_out = X.rvs((self.no_of_out_nodes,
self.no_of_hidden_nodes))
def train(self, input_vector, target_vector):
"""
input_vector and target_vector can be tuples, lists or nda
rrays
"""
# make sure that the vectors have the right shape
input_vector = np.array(input_vector)
input_vector = input_vector.reshape(input_vector.size, 1)
target_vector = np.array(target_vector).reshape(target_vec
tor.size, 1)
output_vector_hidden = activation_function(self.weights_i
n_hidden @ input_vector)
170
output_vector_network = activation_function(self.weights_h
idden_out @ output_vector_hidden)
output_error = target_vector - output_vector_network
tmp = output_error * output_vector_network * (1.0 - outpu
t_vector_network)
self.weights_hidden_out += self.learning_rate * (tmp @ ou
tput_vector_hidden.T)
# calculate hidden errors:
hidden_errors = self.weights_hidden_out.T @ output_error
# update the weights:
tmp = hidden_errors * output_vector_hidden * (1.0 - outpu
t_vector_hidden)
self.weights_in_hidden += self.learning_rate * (tmp @ inpu
t_vector.T)
def run(self, input_vector):
"""
running the network with an input vector 'input_vector'.
'input_vector' can be tuple, list or ndarray
"""
# make sure that input_vector is a column vector:
input_vector = np.array(input_vector)
input_vector = input_vector.reshape(input_vector.size, 1)
input4hidden = activation_function(self.weights_in_hidden
@ input_vector)
output_vector_network = activation_function(self.weights_h
idden_out @ input4hidden)
return output_vector_network
def evaluate(self, data, labels):
"""
Counts how often the actual result corresponds to the
target result.
A result is considered to be correct, if the index of
the maximal value corresponds to the index with the "1"
in the one-hot representation,
e.g.
res = [0.1, 0.132, 0.875]
labels[i] = [0, 0, 1]
"""
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
171
res_max = res.argmax()
if res_max == labels[i].argmax():
corrects += 1
else:
wrongs += 1
return corrects, wrongs
We assume that you save the previous code in a file called neural_networks1.py . We will use it under
this name in the coming examples.
To test this neural network class we need train and test data. We create the data withsklearn.datasets .
make_blobs from
from sklearn.datasets import make_blobs
n_samples = 500
blob_centers = ([2, 6], [6, 2], [7, 7])
n_classes = len(blob_centers)
data, labels = make_blobs(n_samples=n_samples,
centers=blob_centers,
random_state=7)
Let us visualize the previously created data:
import matplotlib.pyplot as plt
colours = ('green', 'red', "yellow")
fig, ax = plt.subplots()
for n_class in range(n_classes):
ax.scatter(data[labels==n_class][:, 0],
data[labels==n_class][:, 1],
c=colours[n_class],
s=40,
label=str(n_class))
172
The labels are wrongly represented. They are in a one-dimensional vector:
labels[:7]
Output:array([2, 2, 1, 0, 2, 0, 1])
We need a one-hot representation for each label. So the labels are represented as
Label
One-Hot Representation
0
(1, 0, 0)
1
(0, 1, 0)
2
(0, 0, 1)
We can easily change the labels with the following commands:
import numpy as np
labels = np.arange(n_classes) == labels.reshape(labels.size, 1)
labels = labels.astype(np.float)
labels[:7]
173
Output:array([[0., 0., 1.],
[0., 0., 1.],
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 1.],
[1., 0., 0.],
[0., 1., 0.]])
We are ready now to create a train and a test data set:
from sklearn.model_selection import train_test_split
res = train_test_split(data, labels,
train_size=0.8,
test_size=0.2,
random_state=42)
train_data, test_data, train_labels, test_labels = res
train_labels[:10]
Output:array([[0., 0., 1.],
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 1.],
[0., 0., 1.],
[1., 0., 0.],
[0., 1., 0.],
[1., 0., 0.],
[1., 0., 0.],
[0., 0., 1.]])
We create a neural network with two input nodes, and three output nodes. One output node for each class:
from neural_networks1 import NeuralNetwork
simple_network = NeuralNetwork(no_of_in_nodes=2,
no_of_out_nodes=3,
no_of_hidden_nodes=5,
learning_rate=0.3)
The next step consists in training our network with the data and labels from our training samples:
for i in range(len(train_data)):
simple_network.train(train_data[i], train_labels[i])
174
We now have to check how well our network has learned. For this purpose, we will use the evaluate function:
simple_network.evaluate(train_data,Output390, 10)
train_labels)
NEURAL NETWORK WITH BIAS NODES
We already introduced the basic idea and necessity of bias nodes in the chapter "Simple Neural Network", in
which we focussed on very simple linearly separable data sets. We learned that a bias node is a node that is
always returning the same output. In other words: It is a node which is not depending on some input and it
does not have any input. The value of a bias node is often set to one, but it can be set to other values as well.
Except for zero, which makes no sense for obvious reasons. If a neural network does not have a bias node in a
given layer, it will not be able to produce output in the next layer that differs from 0 when the feature values
are 0. Generally speaking, we can say that bias nodes are used to increase the flexibility of the network to fit
the data. Usually, there will be not more than one bias node per layer. The only exception is the output layer,
because it makes no sense to add a bias node to this layer.
The following diagram shows the first two layers of our previously used three-layered neural network:
We can see from this diagram that our weight matrix needs one additional column and the bias value has to be
added to the input vector:
175
Again, the situation for the weight matrix between the hidden and the output layer is similar:
The same is true for the corresponding matrix:
The following is a complete Python class implementing our network with bias nodes:
import numpy as np
from scipy.stats import truncnorm
from scipy.special import expit as activation_function
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm(
(low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd)
176
class NeuralNetwork:
def __init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate,
bias=None):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.no_of_out_nodes = no_of_out_nodes
self.learning_rate = learning_rate
self.bias = bias
self.create_weight_matrices()
def create_weight_matrices(self):
""" A method to initialize the weight matrices of the neur
al
network with optional bias nodes"""
bias_node = 1 if self.bias else 0
rad = 1 / np.sqrt(self.no_of_in_nodes + bias_node)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes + bia
s_node))
rad = 1 / np.sqrt(self.no_of_hidden_nodes + bias_node)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_hidden_out = X.rvs((self.no_of_out_nodes,
self.no_of_hidden_nodes
+ bias_node))
def train(self, input_vector, target_vector):
""" input_vector and target_vector can be tuple, list or n
darray """
1)
# make sure that the vectors have the right shap
input_vector = np.array(input_vector)
input_vector = input_vector.reshape(input_vector.size,
if self.bias:
# adding bias node to the end of the input_vector
input_vector = np.concatenate( (input_vector, [[self.b
177
ias]]) )
target_vector = np.array(target_vector).reshape(target_vec
tor.size, 1)
output_vector_hidden = activation_function(self.weights_i
n_hidden @ input_vector)
if self.bias:
output_vector_hidden = np.concatenate( (output_vecto
r_hidden, [[self.bias]]) )
output_vector_network = activation_function(self.weights_h
idden_out @ output_vector_hidden)
output_error = target_vector - output_vector_network
# update the weights:
tmp = output_error * output_vector_network * (1.0 - outpu
t_vector_network)
self.weights_hidden_out += self.learning_rate * (tmp @ ou
tput_vector_hidden.T)
# calculate hidden errors:
hidden_errors = self.weights_hidden_out.T @ output_error
# update the weights:
tmp = hidden_errors * output_vector_hidden * (1.0 - outpu
t_vector_hidden)
if self.bias:
x = (tmp @input_vector.T)[:-1,:]
# last row cut of
f,
else:
x = tmp @ input_vector.T
self.weights_in_hidden += self.learning_rate * x
def run(self, input_vector):
"""
running the network with an input vector 'input_vector'.
'input_vector' can be tuple, list or ndarray
"""
# make sure that input_vector is a column vector:
input_vector = np.array(input_vector)
input_vector = input_vector.reshape(input_vector.size, 1)
if self.bias:
# adding bias node to the end of the inpuy_vector
input_vector = np.concatenate( (input_vector, [[1]]) )
input4hidden = activation_function(self.weights_in_hidden
178
@ input_vector)
if self.bias:
input4hidden = np.concatenate( (input4hidden, [[1]]) )
output_vector_network = activation_function(self.weights_h
idden_out @ input4hidden)
return output_vector_network
def evaluate(self, data, labels):
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
res_max = res.argmax()
if res_max == labels[i].argmax():
corrects += 1
else:
wrongs += 1
return corrects, wrongs
We can use again our previously created classes to test our classifier:
from neural_networks2 import NeuralNetwork
simple_network = NeuralNetwork(no_of_in_nodes=2,
no_of_out_nodes=3,
no_of_hidden_nodes=5,
learning_rate=0.1,
bias=1)
for i in range(len(train_data)):
simple_network.train(train_data[i], train_labels[i])
simple_network.evaluate(train_data, train_labels)
Output382, 18)
EXERCISE
We created in the chapter "Data Creation" a file strange_flowers.txt in the folder data . Create a
Neural Network to classify the 'flowers':
The data looks like this:
0.000,240.000,100.000,3.020
179
253.000,99.000,13.000,3.875
202.000,107.000,6.000,4.1
186.000,84.000,6.000,4.068
0.000,244.000,103.000,3.386
0.000,246.000,98.000,2.955
241.000,103.000,3.000,4.049
236.000,104.000,12.000,3.087
244.000,109.000,1.000,3.111
253.000,97.000,8.000,3.752
231.000,92.000,1.000,3.488
0.000,250.000,103.000,3.379
SOLUTION:
c = np.loadtxt("data/strange_flowers.txt", delimiter=" ")
data = c[:, :-1]
n_classes = data.shape[1]
labels = c[:, -1]
data[:5]
Output:array([[242.
, 117.
,
1.
,
3.87],
[236.
, 104.
,
6.
,
4.11],
[238.
, 116.
,
5.
,
3.9 ],
[248.
, 96.
,
6.
,
3.91],
[252.
, 104.
,
4.
,
3.75]])
labels = np.arange(n_classes) == labels.reshape(labels.size, 1)
labels = labels.astype(np.float)
labels[:3]
Output:array([[0., 1., 0., 0.],
[0., 1., 0., 0.],
[0., 1., 0., 0.]])
We need to scale our data, because unscaled input data can result in a slow or unstable learning process. We
will use the function scale from sklearn/preprocessing . It standardizes a dataset along any axis.
It centers to the mean and component wise scale to unit variance.
from sklearn import preprocessing
data = preprocessing.scale(data)
data[:5]
data.shape
labels.shape
180
Output795, 4)
from sklearn.model_selection import train_test_split
res = train_test_split(data, labels,
train_size=0.8,
test_size=0.2,
random_state=42)
train_data, test_data, train_labels, test_labels = res
train_labels[:10]
Output:array([[0., 0., 1., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.],
[0., 0., 1., 0.],
[0., 0., 0., 1.],
[0., 0., 1., 0.],
[0., 1., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 0., 1.],
[0., 0., 1., 0.]])
from neural_networks2 import NeuralNetwork
simple_network = NeuralNetwork(no_of_in_nodes=4,
no_of_out_nodes=4,
no_of_hidden_nodes=20,
learning_rate=0.3)
for i in range(len(train_data)):
simple_network.train(train_data[i], train_labels[i])
simple_network.evaluate(train_data,Output492, 144)
In [ ]: -
-
Softmax
我们教程中之前实现的神经网络返回的是开区间 (0, 1) 中的浮点值。为了做出最终决定,我们不得不解释输出神经元的结果。值最高的那个是一个可能的候选,但我们还必须结合其他结果来看待它。
很明显,在两类情况下 (\(c_1\) 和 \(c_2\)),结果 (0.013,0.95) 明确表明是 \(c_2\) 类,但另一方面 (0.73,0.89) 则有所不同。在这种情况下,我们可以说“\(c_2\) 比 \(c_1\) 更可能,但 \(c_1\) 仍然具有很高的可能性”。说到可能性:返回的值并不是概率。如果能有一个概率函数进行归一化的输出,那会好得多。这时 Softmax 函数就派上用场了。
Softmax 函数,也称为 softargmax 或归一化指数函数,是一个函数,它接受一个包含 n 个实数的向量作为输入,并将其归一化为由 n 个概率组成的概率分布,这些概率与输入向量的指数成比例。概率分布意味着结果向量的所有分量之和为 1。毋庸置疑,如果输入向量的某些分量是负数或大于 1,在应用 Softmax 后它们将在 (0, 1) 范围内。Softmax 函数通常用于神经网络中,将输出层(非归一化)的结果映射到预测输出类别的概率分布。
Softmax 函数 sigma 由以下公式定义:
其中索引 i 在 中,o 是网络的输出向量 。
我们可以这样实现 Softmax 函数:
Pythonimport numpy as np def softmax(x): """ 对输入 x 应用 softmax """ e_x = np.exp(x) return e_x / e_x.sum() x = np.array([1, 0, 3, 5]) y = softmax(x) print(y, x / x.sum())
输出:
(array([0.01578405, 0.00580663, 0.11662925, 0.86178007]), array([0.11111111, 0. , 0.33333333, 0.55555556]))
避免浮点不稳定性导致的下溢或溢出错误:
Pythonimport numpy as np def softmax(x): """ 对输入 x 应用 softmax """ e_x = np.exp(x - np.max(x)) # 减去最大值以提高数值稳定性 return e_x / e_x.sum() x = np.array([1, 0, 3, 5]) print(softmax(x))
输出:
array([0.01578405, 0.00580663, 0.11662925, 0.86178007])
Pythonx = np.array([0.3, 0.4, 0.00005], np.float64) print(softmax(x)) print(x / x.sum())
Softmax 函数的导数
Softmax 函数可以写成:
每个元素看起来像这样:
Softmax 的导数可以这样计算:
对于每个 i 和 j,偏导数可以求解:
我们将使用商法则,即:
如果\(f(x)=\frac{g(x)}{h(x)},那么 \(f′(x)=\frac{g′(x)\cdot h(x)−g(x) \cdot h′(x)}{(h(x))2}\)
我们可以将 g(x) 设置为 \(e^{o_i}\),将 h(x) 设置为 \[ \sum_{k=1}^{n} e^{o_k} \]。
g(x) 的导数是:
\[ \frac{e^{o_i} \cdot \sum{_{k=1}^{n}} e^{o_k} - e^{o_i} \cdot e^{o_j}}{\left( \sum_{k=1}^{n} e^{o_k} \right)^2} \]
h(x) 的导数是:
\(h′(x)=e^{o_j},∀ k=1,\dots,n\)
现在我们通过分情况讨论来应用商法则:
情况 1: i=j
\[\frac{e^{o_i} \cdot \sum{_{k=1}^n} {e^{o_k}−e^{o_i} \cdot e^{o_j}}}{(\sum{_{k=1}^n}e^{o_k})^2}\]
我们可以将此表达式改写为:
现在我们可以简化第二个商:
如果我们将其与 s_i 的定义进行比较,我们可以将其改写为:
\(s_i \cdot(1−s_j)\)
这与 \(s_i \cdot(1−s_i)\) 相同,因为 。
情况 2: \(i \neq j\)
这可以改写为:
这最终得到:
\(−s_i \cdot s_j\)
我们可以总结这两种情况,并将导数写为:
如果我们使用克罗内克 delta 函数[^1],我们可以消除分情况讨论,即我们“让克罗内克 delta 完成这项工作”:
最后,我们可以计算 Softmax 的导数:
Pythonimport numpy as np def softmax(x): e_x = np.exp(x) return e_x / e_x.sum() s = softmax(np.array([0, 4, 5])) si_sj = - s * s.reshape(3, 1) # s.reshape(3, 1) 将 s 变为列向量进行外积 print(s) print(si_sj) s_der = np.diag(s) + si_sj # np.diag(s) 创建一个以 s 为对角线的矩阵 print(s_der)
输出:
[0.00490169 0.26762315 0.72747516] [[-2.40265555e-05 -1.31180548e-03 -3.56585701e-03] [-1.31180548e-03 -7.16221526e-02 -1.94689196e-01] [-3.56585701e-03 -1.94689196e-01 -5.29220104e-01]] [[ 0.00487766 -0.00131181 -0.00356586] [-0.00131181 0.196001 -0.1946892 ] [-0.00356586 -0.1946892 0.19825505]]
Pythonimport numpy as np from scipy.stats import truncnorm def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm( (low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) @np.vectorize def sigmoid(x): return 1 / (1 + np.e ** -x) def softmax(x): e_x = np.exp(x - np.max(x)) # 提高数值稳定性 return e_x / e_x.sum() class NeuralNetwork: def __init__(self, no_of_in_nodes, no_of_out_nodes, no_of_hidden_nodes, learning_rate, softmax=True): # 默认为True,使用softmax self.no_of_in_nodes = no_of_in_nodes self.no_of_out_nodes = no_of_out_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.learning_rate = learning_rate self.softmax = softmax self.create_weight_matrices() def create_weight_matrices(self): """ 一个初始化神经网络权重矩阵的方法 """ rad = 1 / np.sqrt(self.no_of_in_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes)) rad = 1 / np.sqrt(self.no_of_hidden_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.weights_hidden_out = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes)) def train(self, input_vector, target_vector): """ input_vector 和 target_vector 可以是元组、列表或 ndarray """ # 确保向量具有正确的形状 input_vector = np.array(input_vector).reshape(input_vector.size, 1) target_vector = np.array(target_vector).reshape(target_vector.size, 1) # 前向传播 output_vector_hidden = sigmoid(self.weights_in_hidden @ input_vector) # 应用 Softmax 或 Sigmoid 到输出层 if self.softmax: output_vector_network = softmax(self.weights_hidden_out @ output_vector_hidden) else: output_vector_network = sigmoid(self.weights_hidden_out @ output_vector_hidden) # 计算输出误差 output_error = target_vector - output_vector_network # 更新隐藏层到输出层的权重 if self.softmax: # Softmax 的导数和误差反向传播 # 这里的 tmp 是 (dE/do) * (do/ds) # 其中 dE/do = -(target_vector - output_vector_network) # do/ds 是 Softmax 的雅可比矩阵 ovn = output_vector_network.reshape(output_vector_network.size,) si_sj = - ovn * ovn.reshape(self.no_of_out_nodes, 1) s_der = np.diag(ovn) + si_sj # Softmax 的雅可比矩阵 # tmp 实际上是 (dE/ds) * (ds/dz) 中的 ds/dz # 链式法则:dE/dw_ho = dE/ds * ds/dz * dz/dw_ho # dE/ds = -output_error (损失函数通常为交叉熵时) # ds/dz = Softmax 导数 (s_der) # dz/dw_ho = output_vector_hidden.T # 当使用交叉熵损失时,dE/dz = output_vector_network - target_vector # 这里的 output_error 是 target_vector - output_vector_network # 所以 tmp 应该是 -(output_error) = output_vector_network - target_vector # 但为了与 sigmoid 保持一致,这里用 (target_vector - output_vector_network) # 在 Softmax + 交叉熵损失下,反向传播的误差项简化为 (预测值 - 真实值) # 所以,如果这里损失函数是平方误差,则需要 s_der @ output_error # 但如果损失函数是交叉熵,则直接是 output_error (预测值 - 真实值) # 这里使用平方误差损失,所以 tmp = s_der @ output_error 是正确的 tmp = s_der @ output_error self.weights_hidden_out += self.learning_rate * (tmp @ output_vector_hidden.T) else: # Sigmoid 的导数和误差反向传播 tmp = output_error * output_vector_network * (1.0 - output_vector_network) self.weights_hidden_out += self.learning_rate * (tmp @ output_vector_hidden.T) # 计算隐藏层误差: # 这里 hidden_errors = self.weights_hidden_out.T @ output_error # 这在 Softmax 和 Sigmoid 激活函数下都适用, # 因为它计算的是输出层误差对隐藏层输出的贡献 hidden_errors = self.weights_hidden_out.T @ output_error # 更新输入层到隐藏层的权重: tmp = hidden_errors * output_vector_hidden * (1.0 - output_vector_hidden) self.weights_in_hidden += self.learning_rate * (tmp @ input_vector.T) def run(self, input_vector): """ 使用输入向量 'input_vector' 运行网络。 'input_vector' 可以是元组、列表或 ndarray """ # 确保 input_vector 是一个列向量: input_vector = np.array(input_vector).reshape(input_vector.size, 1) # 前向传播 input4hidden = sigmoid(self.weights_in_hidden @ input_vector) # 应用 Softmax 或 Sigmoid 到输出层 if self.softmax: output_vector_network = softmax(self.weights_hidden_out @ input4hidden) else: output_vector_network = sigmoid(self.weights_hidden_out @ input4hidden) return output_vector_network def evaluate(self, data, labels): corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() # 获取预测类别(概率最高的索引) # 注意:这里的 labels 应该是原始的整数标签,而不是独热编码 # 因为 labels[i] 直接是类别索引 if res_max == labels[i]: corrects += 1 else: wrongs += 1 return corrects, wrongs # --- 测试代码 --- from sklearn.datasets import make_blobs n_samples = 300 samples, labels = make_blobs(n_samples=n_samples, centers=([2, 6], [6, 2]), random_state=0) import matplotlib.pyplot as plt colours = ('green', 'red', 'blue', 'magenta', 'yellow', 'cyan') fig, ax = plt.subplots() for n_class in range(2): ax.scatter(samples[labels==n_class][:, 0], samples[labels==n_class][:, 1], c=colours[n_class], s=40, label=str(n_class)) plt.title("Sample Data") plt.xlabel("Feature 1") plt.ylabel("Feature 2") plt.legend() plt.show() size_of_learn_sample = int(n_samples * 0.8) learn_data = samples[:size_of_learn_sample] test_data = samples[size_of_learn_sample:] # 修改这里,确保测试集是剩余的数据 learn_labels = labels[:size_of_learn_sample] test_labels = labels[size_of_learn_sample:] # 假设这个文件名为 neural_networks_softmax.py # from neural_networks_softmax import NeuralNetwork # 为了方便,这里直接用上面定义的 NeuralNetwork 类 simple_network = NeuralNetwork(no_of_in_nodes=2, no_of_out_nodes=2, no_of_hidden_nodes=5, learning_rate=0.3, softmax=True) # 启用 Softmax print("\n初始化网络后的运行结果 (未训练):") for x_val in [(1, 4), (2, 6), (3, 3), (6, 2)]: y = simple_network.run(x_val) print(f"{x_val} {y.T} (sum: {y.<span class="hljs-built_in">sum</span>():<span class="hljs-number">.4</span>f})") # 打印转置和求和,更清晰 # 转换为独热编码标签进行训练 labels_one_hot = (np.arange(2) == learn_labels.reshape(learn_labels.size, 1)) labels_one_hot = labels_one_hot.astype(np.float64) # 使用 np.float64 print("\n开始训练...") epochs = 500 # 增加训练轮次 for epoch in range(epochs): for i in range(size_of_learn_sample): simple_network.train(learn_data[i], labels_one_hot[i]) print("\n训练后的网络运行结果:") for x_val in [(1, 4), (2, 6), (3, 3), (6, 2)]: y = simple_network.run(x_val) print(f"{x_val} {y.T} (sum: {y.<span class="hljs-built_in">sum</span>():<span class="hljs-number">.4</span>f})") from collections import Counter print("\n训练集评估:") corrects_train, wrongs_train = simple_network.evaluate(learn_data, learn_labels) # 使用原始标签进行评估 print(f"正确分类: {corrects_train}, 错误分类: {wrongs_train}") print(f"准确率: {corrects_train / (corrects_train + wrongs_train):<span class="hljs-number">.4</span>f}") print("\n测试集评估:") corrects_test, wrongs_test = simple_network.evaluate(test_data, test_labels) # 使用原始标签进行评估 print(f"正确分类: {corrects_test}, 错误分类: {wrongs_test}") print(f"准确率: {corrects_test / (corrects_test + wrongs_test):<span class="hljs-number">.4</span>f}")
输出示例(由于随机初始化,具体数字会不同):
初始化网络后的运行结果 (未训练): (1, 4) [[0.5186 0.4814]] (sum: 1.0000) (2, 6) [[0.5057 0.4943]] (sum: 1.0000) (3, 3) [[0.5195 0.4805]] (sum: 1.0000) (6, 2) [[0.5197 0.4803]] (sum: 1.0000) 开始训练... 训练后的网络运行结果: (1, 4) [[0.0105 0.9895]] (sum: 1.0000) (2, 6) [[0.0035 0.9965]] (sum: 1.0000) (3, 3) [[0.9575 0.0425]] (sum: 1.0000) (6, 2) [[0.9998 0.0002]] (sum: 1.0000) 训练集评估: 正确分类: 240, 错误分类: 0 准确率: 1.0000 测试集评估: 正确分类: 60, 错误分类: 0 准确率: 1.0000
注脚
在这个更新后的代码中,我们:
-
在
softmax
函数中加入了数值稳定性的改进(减去最大值)。 -
修改了
NeuralNetwork
类,使其可以通过softmax=True
参数来选择是否在输出层使用 Softmax 激活函数。 -
在
train
方法中根据self.softmax
的值来应用不同的反向传播公式。特别注意,当使用 Softmax 时,如果结合交叉熵损失函数,误差项会大大简化,直接是(预测值 - 真实值)
。然而,此处仍沿用平方误差损失,因此 Softmax 的导数矩阵s_der
仍需乘以output_error
。 -
在
evaluate
方法中,将标签类型改回了原始的整数索引,因为argmax()
返回的是索引,这样更匹配。 -
更新了数据加载和处理部分,确保训练数据和测试数据是不重叠的,并且标签正确地转换为独热编码(训练时)和原始标签(评估时)。
-
增加了训练轮次 (epochs) 以便网络有足够的机会学习。
-
在打印输出结果时,将 Softmax 层的输出求和,以验证其是否为 1,符合概率分布的特性。
Softmax 激活函数在多分类问题中非常有用,因为它能将网络的原始输出转换为易于解释的概率分布,使得我们可以直观地看到每个类别的可能性。
您对 Softmax 的实现和其在神经网络中的应用还有什么疑问吗?
[^1]:
克罗内克 Delta:
SOFTMAX
The previous implementations of neural networks in our tutorial
returned float values in the open interval (0, 1). To make a final
decision we had to interprete the results of the output neurons.
The one with the highest value is a likely candidate but we also
have to see it in relation to the other results. It should be obvious
that in a two classes case (c 1 and c 2) a result (0.013, 0.95) is a
clear vote for the class c 2 but (0.73, 0.89) on the other hand is a
different thing. We could say in this situation 'c2 is more likely
than c1, but c 1 has still a high likelihood'. Talking about
likelihoods: The return values are not probabilities. It would be
a lot better to have a normalized output with a probability
function. Here comes the softmax function into the picture. The
softmax function, also known as softargmax or normalized
exponential function, is a function that takes as input a vector of
n real numbers, and normalizes it into a probability distribution
consisting of n probabilities proportional to the exponentials of
the input vector. A probability distribution implies that the result
vector sums up to 1. Needless to say, if some components of the
input vector are negative or greater than one, they will be in the
range (0, 1) after applying Softmax . The Softmax function is
often used in neural networks, to map the results of the output
layer, which is non-normalized, to a probability distribution over
predicted output classes.
The softmax function σ is defined by the following formula:
e o i
σ(o i) =
∑n eo j
j =1
where the index i is in (0, ..., n-1) and o is the output vector of the network
o = (o 0, o 1, ..., o n − 1)
We can implement the softmax function like this:
import numpy as np
182
def softmax(x):
""" applies softmax to an input x"""
e_x = np.exp(x)
return e_x / e_x.sum()
x = np.array([1, 0, 3, 5])
y = softmax(x)
y, x / x.sum()
Outputarray([0.01578405, 0.00580663, 0.11662925, 0.86178007]),
array([0.11111111, 0.
, 0.33333333, 0.55555556]))
Avoiding underflow or overflow errors due to floating point instability:
import numpy as np
def softmax(x):
""" applies softmax to an input x"""
e_x = np.exp(x - np.max(x))
return e_x / e_x.sum()
softmax(x)
Output:array([0.01578405, 0.00580663, 0.11662925, 0.86178007])
x = np.array([0.3, 0.4, 0.00005], np.float64) print(softmax(x)) print(x / x.sum())
DERIVATE OF SOFTMAX FUNCTION
The softmax function can be written as
S(o) :
[ o o o ⋯
n
1
2
] ?
[ ⋯
s s s 2
n
1
]
Per element it looks like this:
183
e o j
s j(o) =
n
, ∀k = 1, ⋯, n
∑ e ok
k =1
The derivative of softmax can be calculated like this:
∂O
∂S
=
[ ∂s1
∂o ∂sn
∂o ⋯
1
1
⋯
⋯
∂on
∂on
∂s ∂s 1
n
]
The partial derivatives can be solved for every i and j:
e oi
∂
∂s i
∑n
k =1
e o k
=
∂oj
∂o j
We will use the quotien rule, i.e.
the derivative of
g(x)
f(x) =
h(x)
is
g ′ (x) ⋅ h(x) − g(x) ⋅ h ′ (x)
f ′ (x) =
(h(x) 2
We can set g(x) to e o i and h(x) to ∑ n
e o k
k =1
The derivative of g(x) is
g ′ (x) =
{
e 0,
o i,
if otherwise
i = j
and the derivative of h(x) is
184
h ′ (x) = e o j, ∀k = 1, ⋯, n
Let's apply the quotient rule by case differentiation now:
1. case: i = j:
e oi ⋅ ∑ n
e o k − e o i ⋅ e o j
k =1
( ∑ n
e o k) 2
k =1
We can rewrite this expression as
∑ n
e o k − e o j
eoi
k =1
⋅
∑ n
e o k
∑ n
e o k
k =1
k =1
Now we can reduce the second quotient:
e o i
e o j
⋅
(1
−
)
∑ n
e o k
∑n eok
k =1
k =1
If we compare this expression with the Definition of si, we can rewrite it to:
s i ⋅ (1 − s j)
which is the same as
s i ⋅ (1 − s i)
because i = j.
1. case: i ≠ j:
0 ⋅ ∑ n
e o k − e o i ⋅ e o j
k =1
( ∑ n
e o k) 2
k =1
this can be rewritten as:
eoi
eoj
−
⋅
∑ n
e o k ∑ n e o k
k =1
k =1
this gives us finally:
185
− s i ⋅ s j
We can summarize these two cases and write the derivative as:
g ′ (x) =
{
si − s ⋅ i (1 ⋅ sj,
− si),
if otherwise
i = j
If we use the Kronecker delta function1, we can get rid of the case differentiation, i.e. we "let the Kronecker
delta do this work":
∂s i
= s i(δ ij − s j)
∂o j
Finally we can calculate the derivative of softmax:
∂O
∂S
=
[
s s s1(δ11 2(δ n(δ n1 21 ⋯
− − − s1)
s s1)
1)
s s s 2(δ 1(δ n(δ 22 n2 12 − − − s2)
s2)
s2)
⋯
⋯
⋯
s s s 1(δ n(δ 2(δ nn 1n 2n − − − s s s n)
n)
n)
]
import numpy as np
def softmax(x):
e_x = np.exp(x)
return e_x / e_x.sum()
s = softmax(np.array([0, 4, 5]))
si_sj = - s * s.reshape(3, 1)
print(s)
print(si_sj)
s_der = np.diag(s) + si_sj
s_der
186
[0.00490169 0.26762315 0.72747516]
[[-2.40265555e-05 -1.31180548e-03 -3.56585701e-03]
[-1.31180548e-03 -7.16221526e-02 -1.94689196e-01]
[-3.56585701e-03 -1.94689196e-01 -5.29220104e-01]]
Output:array([[ 0.00487766, -0.00131181, -0.00356586],
[-0.00131181, 0.196001 , -0.1946892 ],
[-0.00356586, -0.1946892 , 0.19825505]])
import numpy as np
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm(
(low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd)
@np.vectorize
def sigmoid(x):
return 1 / (1 + np.e ** -x)
def softmax(x):
e_x = np.exp(x)
return e_x / e_x.sum()
class NeuralNetwork:
def __init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate,
softmax=True):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_out_nodes = no_of_out_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.learning_rate = learning_rate
self.softmax = softmax
self.create_weight_matrices()
def create_weight_matrices(self):
""" A method to initialize the weight matrices of the neur
al network"""
rad = 1 / np.sqrt(self.no_of_in_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
187
self.weights_in_hidden = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes))
rad = 1 / np.sqrt(self.no_of_hidden_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_hidden_out = X.rvs((self.no_of_out_nodes,
self.no_of_hidden_nodes))
def train(self, input_vector, target_vector):
"""
input_vector and target_vector can be tuples, lists or nda
rrays
"""
# make sure that the vectors have the right shape
input_vector = np.array(input_vector)
input_vector = input_vector.reshape(input_vector.size, 1)
target_vector = np.array(target_vector).reshape(target_vec
tor.size, 1)
output_vector_hidden = sigmoid(self.weights_in_hidden @ in
put_vector)
if self.softmax:
output_vector_network = softmax(self.weights_hidden_ou
t @ output_vector_hidden)
else:
output_vector_network = sigmoid(self.weights_hidden_ou
t @ output_vector_hidden)
output_error = target_vector - output_vector_network
if self.softmax:
ovn = output_vector_network.reshape(output_vector_netw
ork.size,)
si_sj = - ovn * ovn.reshape(self.no_of_out_nodes, 1)
s_der = np.diag(ovn) + si_sj
tmp = s_der @ output_error
self.weights_hidden_out += self.learning_rate * (tmp
@ output_vector_hidden.T)
else:
tmp = output_error * output_vector_network * (1.0 - ou
tput_vector_network)
self.weights_hidden_out += self.learning_rate * (tmp
@ output_vector_hidden.T)
188
# calculate hidden errors:
hidden_errors = self.weights_hidden_out.T @ output_error
# update the weights:
tmp = hidden_errors * output_vector_hidden * (1.0 - outpu
t_vector_hidden)
self.weights_in_hidden += self.learning_rate * (tmp @ inpu
t_vector.T)
def run(self, input_vector):
"""
running the network with an input vector 'input_vector'.
'input_vector' can be tuple, list or ndarray
"""
# make sure that input_vector is a column vector:
input_vector = np.array(input_vector)
input_vector = input_vector.reshape(input_vector.size, 1)
input4hidden = sigmoid(self.weights_in_hidden @ input_vect
or)
if self.softmax:
output_vector_network = softmax(self.weights_hidden_ou
t @ input4hidden)
else:
output_vector_network = sigmoid(self.weights_hidden_ou
t @ input4hidden)
return output_vector_network
def evaluate(self, data, labels):
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
res_max = res.argmax()
if res_max == labels[i]:
corrects += 1
else:
wrongs += 1
return corrects, wrongs
from sklearn.datasets import make_blobs
n_samples = 300
samples, labels = make_blobs(n_samples=n_samples,
centers=([2, 6], [6, 2]),
random_state=0)
189
import matplotlib.pyplot as plt
colours = ('green', 'red', 'blue', 'magenta', 'yellow', 'cyan')
fig, ax = plt.subplots()
for n_class in range(2):
ax.scatter(samples[labels==n_class][:, 0], samples[labels==n_c
lass][:, 1],
c=colours[n_class], s=40, label=str(n_class))
size_of_learn_sample = int(n_samples * 0.8)
learn_data = samples[:size_of_learn_sample]
test_data = samples[-size_of_learn_sample:]
from neural_networks_softmax import NeuralNetwork
simple_network = NeuralNetwork(no_of_in_nodes=2,
no_of_out_nodes=2,
no_of_hidden_nodes=5,
learning_rate=0.3,
softmax=True)
for x in [(1, 4), (2, 6), (3, 3), (6, 2)]:
y = simple_network.run(x)
print(x, y, s.sum())
(1, 4) [[0.53325729]
[0.46674271]] 1.0
(2, 6) [[0.50669849]
[0.49330151]] 1.0
(3, 3) [[0.53050147]
[0.46949853]] 1.0
(6, 2) [[0.52530293]
[0.47469707]] 1.0
labels_one_hot = (np.arange(2) == labels.reshape(labels.size, 1))
labels_one_hot = labels_one_hot.astype(np.float)
for i in range(size_of_learn_sample):
#print(learn_data[i], labels[i], labels_one_hot[i])
simple_network.train(learn_data[i],
labels_one_hot[i])
from collections import Counter
190
evaluation = Counter()
simple_network.evaluate(learn_data, labels)
Output236, 4)
FOOTNOTES
1
Kronecker delta:
δ ij =
{
1,
0,
if if i i = ≠ j
j -
-
介绍
在我们之前的机器学习教程章节(《使用 Python 和 Numpy 的神经网络》 和 《从零开始的神经网络》)中,我们实现了各种算法,但没有适当地衡量输出的质量。主要原因是,我们使用了非常简单和小型的数据集进行学习和测试。在《神经网络:MNIST 测试》这一章中,我们将使用大型数据集和十个类别,因此我们需要合适的评估工具。在本章中,我们将介绍混淆矩阵的概念:
混淆矩阵是一个矩阵(表格),可用于衡量机器学习算法(通常是监督学习算法)的性能。混淆矩阵的每一行代表实际类别的实例,每一列代表预测类别的实例。在本教程的这一章中,我们保持这种方式,但也可以反过来,即行表示预测类别,列表示实际类别。混淆矩阵这个名称反映了一个事实,即它使我们很容易看到分类算法中发生了哪些混淆。例如,算法本应将样本预测为 c_i,因为实际类别是 c_i,但算法却输出 c_j。在这种错误标记的情况下,当构建混淆矩阵时,元素 cm[i,j] 将加一。
我们将在下面的类中定义计算混淆矩阵、精确率 (precision) 和召回率 (recall) 的方法。
两类情况
在两类情况下,即“负类”和“正类”,混淆矩阵可能看起来像这样:
实际
预测
负类
正类
负类
11
0
正类
1
12
矩阵的字段含义如下:
实际
预测
负类
正类
负类
TN
FP
真负类
假正类
正类
FN
TP
假负类
真正类
我们现在可以定义一些机器学习中使用的重要性能指标:
准确率 (Accuracy):
\(AC=\frac{TN+TP}{TN+FP+FN+TP}\)
准确率并不总是衡量性能的足够指标。假设我们有 1000 个样本。其中 995 个是负例,5 个是正例。让我们进一步假设我们有一个分类器,无论它遇到什么,都将其分类为负例。准确率将达到惊人的 99.5%,尽管该分类器未能识别任何正例。
召回率 (Recall),又称真阳性率 (True Positive Rate):
\(recall=\frac{TP}{FN+TP}\)
真负性率 (True Negative Rate):
\(TNR=\frac{TN}{TN+FP}\)
精确率 (Precision):
\(precision=\frac{TP}{FP+TP}\)
多类情况
为了衡量机器学习算法的结果,之前的混淆矩阵将不足够。我们需要将其推广到多类情况。
假设我们有 25 只动物的样本,例如 7 只猫、8 只狗和 10 条蛇(很可能是蟒蛇)。我们的识别算法的混淆矩阵可能如下表所示:
实际
预测
狗
猫
蛇
狗
6
2
0
猫
1
6
0
蛇
1
1
8
在这个混淆矩阵中,系统正确预测了八只实际狗中的六只,但在两种情况下它把狗当作了猫。七只实际猫中有六只被正确识别,但在一种情况下猫被当作了狗。通常,很难把蛇当作狗或猫,但我们的分类器在两种情况下却发生了这种情况。然而,十条蛇中有八条被正确识别。(这个机器学习算法很可能不是用 Python 程序编写的,因为 Python 应该能够正确识别自己的物种
)
你可以看到所有正确预测都位于表格的对角线上,因此预测错误可以很容易地在表格中找到,因为它们将由对角线以外的值表示。
我们可以将其推广到多类情况。为此,我们对混淆矩阵的行和列进行总结。鉴于矩阵如上所示,即矩阵的给定行对应于“真实值”的特定值,我们有:
\(Precision_i=\frac{M_ii}{\sum_{j} M_ji}\)
\(Recall_i=\frac{M_ii}{sum_{j} M_ij}\)
这意味着,精确率是算法正确预测类别 i 的情况占所有算法预测 i 的实例(正确和不正确)的比例。另一方面,召回率是算法正确预测 i 的情况占所有标记为 i 的情况的比例。
让我们将其应用于我们的例子:
我们动物的精确率可以计算为:
\(precision_{dogs}=6/(6+1+1)=3/4=0.75\)
\(precision_{cats}=6/(2+6+1)=6/9=0.67\)
\(precision_{snakes}=8/(0+0+8)=1\)
召回率这样计算:
\(recall_{dogs}=6/(6+2+0)=3/4=0.75\)
\(recall_{cats}=6/(1+6+0)=6/7=0.86\)
\(recall_{snakes}=8/(1+1+8)=4/5=0.8\)
示例
我们现在准备用 Python 编写代码。以下代码显示了一个多类机器学习问题的混淆矩阵,该问题有十个标签,例如用于识别手写数字的算法。
如果您不熟悉 Numpy 和 Numpy 数组,我们推荐您阅读我们的 Numpy 教程。
Pythonimport numpy as np cm = np.array( [[5825, 1, 49, 23, 7, 46, 30, 12, 21, 26], [ 1, 6654, 48, 25, 10, 32, 19, 62, 111, 10], [ 2, 20, 5561, 69, 13, 10, 2, 45, 18, 2], [ 6, 26, 99, 5786, 5, 111, 1, 41, 110, 79], [ 4, 10, 43, 6, 5533, 32, 11, 53, 34, 79], [ 3, 1, 2, 56, 0, 4954, 23, 0, 12, 5], [ 31, 4, 42, 22, 45, 103, 5806, 3, 34, 3], [ 0, 4, 30, 29, 5, 6, 0, 5817, 2, 28], [ 35, 6, 63, 58, 8, 59, 26, 13, 5394, 24], [ 16, 16, 21, 57, 216, 68, 0, 219, 115, 5693]]) # 'precision' 和 'recall' 函数计算单个标签的值,而 'precision_macro_average' 计算整个分类问题的精确率。 def precision(label, confusion_matrix): col = confusion_matrix[:, label] return confusion_matrix[label, label] / col.sum() def recall(label, confusion_matrix): row = confusion_matrix[label, :] return confusion_matrix[label, label] / row.sum() def precision_macro_average(confusion_matrix): rows, columns = confusion_matrix.shape sum_of_precisions = 0 for label in range(rows): sum_of_precisions += precision(label, confusion_matrix) return sum_of_precisions / rows def recall_macro_average(confusion_matrix): rows, columns = confusion_matrix.shape sum_of_recalls = 0 for label in range(columns): sum_of_recalls += recall(label, confusion_matrix) return sum_of_recalls / columns print("label precision recall") for label in range(10): print(f"{label:5d} {precision(label, cm):<span class="hljs-number">9.3</span>f} {recall(label, cm):<span class="hljs-number">6.3</span>f}") print("precision total:", precision_macro_average(cm)) print("recall total:", recall_macro_average(cm)) def accuracy(confusion_matrix): diagonal_sum = confusion_matrix.trace() # 对角线元素之和(正确预测) sum_of_all_elements = confusion_matrix.sum() # 所有元素之和(总样本数) return diagonal_sum / sum_of_all_elements print("accuracy:", accuracy(cm))
输出:
label precision recall 0 0.983 0.964 1 0.987 0.954 2 0.933 0.968 3 0.944 0.924 4 0.947 0.953 5 0.914 0.980 6 0.981 0.953 7 0.928 0.982 8 0.922 0.949 9 0.957 0.887 precision total: 0.949688556405 recall total: 0.951453154788 accuracy: 0.95038333333333336
In the previous chapters of our Machine
Learning tutorial (Neural Networks with
Python and Numpy and Neural Networks
from Scratch ) we implemented various
algorithms, but we didn't properly
measure the quality of the output. The
main reason was that we used very simple
and small datasets to learn and test. In the
chapter Neural Network: Testing with
MNIST, we will work with large datasets
and ten classes, so we need proper
evaluations tools. We will introduce in
this chapter the concepts of the confusion
matrix:
A confusion matrix is a matrix (table) that can be used to measure the performance of an machine learning
algorithm, usually a supervised learning one. Each row of the confusion matrix represents the instances of an
actual class and each column represents the instances of a predicted class. This is the way we keep it in this
chapter of our tutorial, but it can be the other way around as well, i.e. rows for predicted classes and columns
for actual classes. The name confusion matrix reflects the fact that it makes it easy for us to see what kind of
confusions occur in our classification algorithms. For example the algorithms should have predicted a sample
as c i because the actual class is c i, but the algorithm came out with cj. In this case of mislabelling the element
cm[i, j] will be incremented by one, when the confusion matrix is constructed.
We will define methods to calculate the confusion matrix, precision and recall in the following class.
2-CLASS CASE
In a 2-class case, i.e. "negative" and "positive", the confusion matrix may look like this:
predicted
actual
negative
positive
negative
11
0
positive
1
12
192
The fields of the matrix mean the following:
predicted
actual
negative
positive
negative
TN
FP
True positive
False Positive
positive
FN
TP
False negative
True positive
We can define now some important performance measures used in machine learning:
Accuracy:
TN + TP
AC =
TN + FP + FN + TP
The accuracy is not always an adequate performance measure. Let us assume we have 1000 samples. 995 of
these are negative and 5 are positive cases. Let us further assume we have a classifier, which classifies
whatever it will be presented as negative. The accuracy will be a surprising 99.5%, even though the classifier
could not recognize any positive samples.
Recall aka. True Positive Rate:
TP
recall =
FN + TP
True Negative Rate:
FP
TNR =
TN + FP
Precision:
TP
precision :
FP + TP
193
MULTI-CLASS CASE
To measure the results of machine learning algorithms, the previous confusion matrix will not be sufficient.
We will need a generalization for the multi-class case.
Let us assume that we have a sample of 25 animals, e.g. 7 cats, 8 dogs, and 10 snakes, most probably Python
snakes. The confusion matrix of our recognition algorithm may look like the following table:
predicted
actual
dog
cat
snake
dog
6
2
0
cat
1
6
0
snake
1
1
8
In this confusion matrix, the system correctly predicted six of the eight actual dogs, but in two cases it took a
dog for a cat. The seven acutal cats were correctly recognized in six cases but in one case a cat was taken to be
a dog. Usually, it is hard to take a snake for a dog or a cat, but this is what happened to our classifier in two
cases. Yet, eight out of ten snakes had been correctly recognized. (Most probably this machine learning
algorithm was not written in a Python program, because Python should properly recognize its own species)
You can see that all correct predictions are located in the diagonal of the table, so prediction errors can be
easily found in the table, as they will be represented by values outside the diagonal.
We can generalize this to the multi-class case. To do this we summarize over the rows and columns of the
confusion matrix. Given that the matrix is oriented as above, i.e., that a given row of the matrix corresponds to
specific value for the "truth", we have:
Precision i =
M ii
∑ jMji
Mii
Recalli =
∑jM ij
This means, precision is the fraction of cases where the algorithm correctly predicted class i out of all
instances where the algorithm predicted i (correctly and incorrectly). recall on the other hand is the fraction of
cases where the algorithm correctly predicted i out of all of the cases which are labelled as i.
Let us apply this to our example:
194
The precision for our animals can be calculated as
precisiondogs = 6 / (6 + 1 + 1) = 3 / 4 = 0.75
precision = 6 / (2 + 6 + 1) = 6 / 9 = 0.67
catsprecision snakes = 8 / (0 + 0 + 8) = 1
The recall is calculated like this:
recall = 6 / (6 + 2 + 0) = 3 / 4 = 0.75
dogsrecall cats = 6 / (1 + 6 + 0) = 6 / 7 = 0.86
recall = 8 / (1 + 1 + 8) = 4 / 5 = 0.8
snakesEXAMPLE
We are ready now to code this into Python. The following code shows a confusion matrix for a multi-class
machine learning problem with ten labels, so for example an algorithms for recognizing the ten digits from
handwritten characters.
If you are not familiar with Numpy and Numpy arrays, we recommend our tutorial on Numpy.
import numpy as np
cm = np.array(
[[5825,
1,
49,
23,
7,
46,
30,
12,
21,
26],
[
1, 6654,
48,
25,
10,
32,
19,
62, 111,
10],
[
2,
20, 5561,
69,
13,
10,
2,
45,
18,
2],
[
6,
26,
99, 5786,
5, 111,
1,
41, 110,
79],
[
4,
10,
43,
6, 5533,
32,
11,
53,
34,
79],
[
3,
1,
2,
56,
0, 4954,
23,
0,
12,
5],
[ 31,
4,
42,
22,
45, 103, 5806,
3,
34,
3],
[
0,
4,
30,
29,
5,
6,
0, 5817,
2,
28],
[ 35,
6,
63,
58,
8,
59,
26,
13, 5394,
24],
[ 16,
16,
21,
57, 216,
68,
0, 219, 115, 5693]])
The functions 'precision' and 'recall' calculate values for a label, whereas the function
'precision_macro_average' the precision for the whole classification problem calculates.
def precision(label, confusion_matrix):
col = confusion_matrix[:, label]
return confusion_matrix[label, label] / col.sum()
195
def recall(label, confusion_matrix):
row = confusion_matrix[label, :]
return confusion_matrix[label, label] / row.sum()
def precision_macro_average(confusion_matrix):
rows, columns = confusion_matrix.shape
sum_of_precisions = 0
for label in range(rows):
sum_of_precisions += precision(label, confusion_matrix)
return sum_of_precisions / rows
def recall_macro_average(confusion_matrix):
rows, columns = confusion_matrix.shape
sum_of_recalls = 0
for label in range(columns):
sum_of_recalls += recall(label, confusion_matrix)
return sum_of_recalls / columns
print("label precision recall")
for label in range(10):
print(f"{label:5d} {precision(label, cm):9.3f} {recall(label,<br>cm):6.3f}")
label precision recall
0
0.983 0.964
1
0.987 0.954
2
0.933 0.968
3
0.944 0.924
4
0.947 0.953
5
0.914 0.980
6
0.981 0.953
7
0.928 0.982
8
0.922 0.949
9
0.957 0.887
print("precision total:", precision_macro_average(cm))
print("recall total:", recall_macro_average(cm))
precision total: 0.949688556405
recall total: 0.951453154788
def accuracy(confusion_matrix):
diagonal_sum = confusion_matrix.trace()
sum_of_all_elements = confusion_matrix.sum()
196
return diagonal_sum / sum_of_all_elements
accuracy(cm)
Output:0.95038333333333336 -
使用 MNIST
MNIST 数据库(修改后的美国国家标准与技术研究院数据库)的手写数字包含一个 60,000 个示例的训练集和一个 10,000 个示例的测试集。它是 NIST 提供的更大数据集的子集。此外,NIST 的黑白图像经过大小归一化和居中处理,以适应 28x28 像素的包围盒并进行抗锯齿处理,这引入了灰度级别。
这个数据库因其在机器学习和图像处理领域的训练和测试而备受青睐。它是原始 NIST 数据集的重混子集。60,000 张训练图像中的一半来自 NIST 的测试数据集,另一半来自 NIST 的训练集。10,000 张测试集图像也以类似方式组装。
MNIST 数据集被研究人员用于测试和比较他们的研究结果。文献中最低的错误率低至 0.21%。
读取 MNIST 数据集
数据集中的图像大小为 28 x 28 像素。它们保存在 CSV 数据文件
mnist_train.csv
和mnist_test.csv
中。这些文件中的每一行都包含一个图像,即 785 个介于 0 到 255 之间的数字。
每行的第一个数字是标签,即图像中描绘的数字。接下来的 784 个数字是 28 x 28 图像的像素值。
Pythonimport numpy as np import matplotlib.pyplot as plt image_size = 28 # 宽度和长度 no_of_different_labels = 10 # 即 0, 1, 2, ..., 9 image_pixels = image_size * image_size # 28 * 28 = 784 像素 data_path = "data/mnist/" # 确保 'data/mnist/' 路径下有 csv 文件 # 加载训练数据和测试数据 train_data = np.loadtxt(data_path + "mnist_train.csv", delimiter=",") test_data = np.loadtxt(data_path + "mnist_test.csv", delimiter=",") # 查看测试数据的前10行 print("测试数据前10行:") print(test_data[:10]) # 检查数据中值为255的元素(可选) # print("值为255的元素数量:", test_data[test_data==255].size) # 检查测试数据的形状 print("测试数据形状:", test_data.shape)
输出:
测试数据前10行: [[7. 0. 0. ... 0. 0. 0.] [2. 0. 0. ... 0. 0. 0.] [1. 0. 0. ... 0. 0. 0.] ... [9. 0. 0. ... 0. 0. 0.] [5. 0. 0. ... 0. 0. 0.] [9. 0. 0. ... 0. 0. 0.]] 测试数据形状: (10000, 785)
MNIST 数据集的图像是灰度图像,像素值介于 0 到 255 之间(包括两端值)。我们将通过将每个像素乘以 0.99 / 255 并加上 0.01 来将这些值映射到 [0.01, 1] 的区间。这样,我们避免了输入值为 0 的情况,正如我们在介绍章节中看到的那样,0 值会阻止权重更新。
Pythonfac = 0.99 / 255 # 提取图像数据(除了第一列的标签),并进行缩放和偏移 train_imgs = np.asfarray(train_data[:, 1:]) * fac + 0.01 test_imgs = np.asfarray(test_data[:, 1:]) * fac + 0.01 # 提取标签数据(第一列) train_labels = np.asfarray(train_data[:, :1]) test_labels = np.asfarray(test_data[:, :1])
在我们的计算中,我们需要独热 (one-hot) 表示的标签。我们有 0 到 9 的 10 个数字,即
lr = np.arange(10)
。将标签转换为独热表示可以通过命令 (lr==label).astype(np.int) 实现。
我们通过以下示例演示:
Pythonimport numpy as np lr = np.arange(10) for label in range(10): one_hot = (lr==label).astype(np.int) # 使用 np.int 或 np.int32/np.int64 print("label: ", label, " in one-hot representation: ", one_hot)
输出:
label: 0 in one-hot representation: [1 0 0 0 0 0 0 0 0 0] label: 1 in one-hot representation: [0 1 0 0 0 0 0 0 0 0] label: 2 in one-hot representation: [0 0 1 0 0 0 0 0 0 0] label: 3 in one-hot representation: [0 0 0 1 0 0 0 0 0 0] label: 4 in one-hot representation: [0 0 0 0 1 0 0 0 0 0] label: 5 in one-hot representation: [0 0 0 0 0 1 0 0 0 0] label: 6 in one-hot representation: [0 0 0 0 0 0 1 0 0 0] label: 7 in one-hot representation: [0 0 0 0 0 0 0 1 0 0] label: 8 in one-hot representation: [0 0 0 0 0 0 0 0 1 0] label: 9 in one-hot representation: [0 0 0 0 0 0 0 0 0 1]
现在我们准备将带标签的图像转换为独热表示。我们创建 0.01 和 0.99 而不是 0 和 1,这对于我们的计算会更好:
Pythonlr = np.arange(no_of_different_labels) # 将标签转换为独热表示 train_labels_one_hot = (lr==train_labels).astype(np.float64) # 使用 np.float64 test_labels_one_hot = (lr==test_labels).astype(np.float64) # 使用 np.float64 # 将独热标签中的 0 变为 0.01,1 变为 0.99 train_labels_one_hot[train_labels_one_hot==0] = 0.01 train_labels_one_hot[train_labels_one_hot==1] = 0.99 test_labels_one_hot[test_labels_one_hot==0] = 0.01 test_labels_one_hot[test_labels_one_hot==1] = 0.99
在我们开始将 MNIST 数据集用于神经网络之前,我们先看看一些图像:
Pythonfor i in range(10): img = train_imgs[i].reshape((28,28)) plt.imshow(img, cmap="Greys") plt.title(f"Label: {int(train_labels[i][0])}") # 显示图像对应的标签 plt.show()
快速重新加载的数据转储
您可能已经注意到从 CSV 文件读取数据相当慢。
我们将使用 pickle 模块的 dump 函数以二进制格式保存数据:
Pythonimport pickle # 确保 'data/mnist/' 目录存在 import os os.makedirs("data/mnist/", exist_ok=True) with open("data/mnist/pickled_mnist.pkl", "bw") as fh: data = (train_imgs, test_imgs, train_labels, test_labels, train_labels_one_hot, test_labels_one_hot) pickle.dump(data, fh)
现在我们可以使用
pickle.load
读取数据了。这比使用loadtxt
读取 CSV 文件快得多:Pythonimport pickle with open("data/mnist/pickled_mnist.pkl", "br") as fh: data = pickle.load(fh) train_imgs = data[0] test_imgs = data[1] train_labels = data[2] test_labels = data[3] train_labels_one_hot = data[4] test_labels_one_hot = data[5] image_size = 28 # 宽度和长度 no_of_different_labels = 10 # 即 0, 1, 2, ..., 9 image_pixels = image_size * image_size
数据分类
我们将使用以下神经网络类进行我们的首次分类:
Pythonimport numpy as np @np.vectorize def sigmoid(x): return 1 / (1 + np.e ** -x) activation_function = sigmoid from scipy.stats import truncnorm def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm((low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) class NeuralNetwork: def __init__(self, no_of_in_nodes, no_of_out_nodes, no_of_hidden_nodes, learning_rate): self.no_of_in_nodes = no_of_in_nodes self.no_of_out_nodes = no_of_out_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.learning_rate = learning_rate self.create_weight_matrices() def create_weight_matrices(self): """ 一个初始化神经网络权重矩阵的方法 """ rad = 1 / np.sqrt(self.no_of_in_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.wih = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes)) # 输入层到隐藏层权重 rad = 1 / np.sqrt(self.no_of_hidden_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.who = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes)) # 隐藏层到输出层权重 def train(self, input_vector, target_vector): """ input_vector 和 target_vector 可以是元组、列表或 ndarray """ # 将输入和目标向量转换为列向量 input_vector = np.array(input_vector, ndmin=2).T target_vector = np.array(target_vector, ndmin=2).T # 前向传播 output_vector1 = np.dot(self.wih, input_vector) output_hidden = activation_function(output_vector1) output_vector2 = np.dot(self.who, output_hidden) output_network = activation_function(output_vector2) # 计算误差 output_errors = target_vector - output_network # 更新隐藏层到输出层的权重 (who) tmp = output_errors * output_network * (1.0 - output_network) tmp = self.learning_rate * np.dot(tmp, output_hidden.T) self.who += tmp # 计算隐藏层误差 hidden_errors = np.dot(self.who.T, output_errors) # 更新输入层到隐藏层的权重 (wih) tmp = hidden_errors * output_hidden * (1.0 - output_hidden) self.wih += self.learning_rate * np.dot(tmp, input_vector.T) def run(self, input_vector): # input_vector 可以是元组、列表或 ndarray input_vector = np.array(input_vector, ndmin=2).T # 前向传播 output_vector = np.dot(self.wih, input_vector) output_vector = activation_function(output_vector) output_vector = np.dot(self.who, output_vector) output_vector = activation_function(output_vector) return output_vector def confusion_matrix(self, data_array, labels): cm = np.zeros((10, 10), int) # 10x10 的混淆矩阵 for i in range(len(data_array)): res = self.run(data_array[i]) res_max = res.argmax() # 预测的类别(索引) target = int(labels[i][0]) # 真实的类别(从 [标签.] 中提取整数) cm[res_max, target] += 1 # 预测为 res_max,实际为 target return cm def precision(self, label, confusion_matrix): col = confusion_matrix[:, label] # 预测为该标签的所有样本(列) return confusion_matrix[label, label] / col.sum() def recall(self, label, confusion_matrix): row = confusion_matrix[label, :] # 实际为该标签的所有样本(行) return confusion_matrix[label, label] / row.sum() def evaluate(self, data, labels): corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() # 预测的类别 if res_max == int(labels[i][0]): # 比较预测类别和实际类别(提取整数值) corrects += 1 else: wrongs += 1 return corrects, wrongs # --- 实例化并训练神经网络 --- ANN = NeuralNetwork(no_of_in_nodes = image_pixels, no_of_out_nodes = 10, no_of_hidden_nodes = 100, # 隐藏层节点数 learning_rate = 0.1) # 学习率 # 训练网络 print("开始训练网络...") epochs = 5 # 增加训练轮次,可以进一步提高准确率 for epoch in range(epochs): print(f"Epoch {epoch+1}/{epochs}") for i in range(len(train_imgs)): ANN.train(train_imgs[i], train_labels_one_hot[i]) print("训练完成。") # 对测试集前20个样本进行预测并打印结果 print("\n测试集前20个样本的预测结果:") for i in range(20): res = ANN.run(test_imgs[i]) print(f"真实标签: {int(test_labels[i][0])}, 预测标签: {np.argmax(res)}, 最大预测概率: {np.max(res):.4f}") # 评估训练集和测试集的准确率 corrects_train, wrongs_train = ANN.evaluate(train_imgs, train_labels) print("\n训练集准确率: ", corrects_train / ( corrects_train + wrongs_train)) corrects_test, wrongs_test = ANN.evaluate(test_imgs, test_labels) print("测试集准确率: ", corrects_test / ( corrects_test + wrongs_test)) # 计算混淆矩阵 cm = ANN.confusion_matrix(train_imgs, train_labels) # 可以计算训练集或测试集的混淆矩阵 print("\n训练集混淆矩阵:") print(cm) # 计算并打印每个数字的精确率和召回率 print("\n每个数字的精确率和召回率:") for i in range(10): # 确保分母不为零,避免除以零的警告 prec = ANN.precision(i, cm) if cm[:, i].sum() > 0 else np.nan rec = ANN.recall(i, cm) if cm[i, :].sum() > 0 else np.nan print(f"数字: {i}, 精确率: {prec:.4f}, 召回率: {rec:.4f}")
输出示例(由于随机初始化和训练过程,具体数字可能会略有不同):
开始训练网络... Epoch 1/5 ... Epoch 5/5 训练完成。 测试集前20个样本的预测结果: 真实标签: 7, 预测标签: 7, 最大预测概率: 0.9829 真实标签: 2, 预测标签: 2, 最大预测概率: 0.7373 真实标签: 1, 预测标签: 1, 最大预测概率: 0.9882 真实标签: 0, 预测标签: 0, 最大预测概率: 0.9873 真实标签: 4, 预测标签: 4, 最大预测概率: 0.9456 真实标签: 1, 预测标签: 1, 最大预测概率: 0.9880 真实标签: 4, 预测标签: 4, 最大预测概率: 0.9766 真实标签: 9, 预测标签: 9, 最大预测概率: 0.9649 真实标签: 5, 预测标签: 6, 最大预测概率: 0.3662 <-- 这是一个错误分类示例 真实标签: 9, 预测标签: 9, 最大预测概率: 0.9849 真实标签: 0, 预测标签: 0, 最大预测概率: 0.9204 真实标签: 6, 预测标签: 6, 最大预测概率: 0.8898 真实标签: 9, 预测标签: 9, 最大预测概率: 0.9937 真实标签: 0, 预测标签: 0, 最大预测概率: 0.9832 真实标签: 1, 预测标签: 1, 最大预测概率: 0.9888 真实标签: 5, 预测标签: 5, 最大预测概率: 0.9157 真实标签: 9, 预测标签: 9, 最大预测概率: 0.9813 真实标签: 7, 预测标签: 7, 最大预测概率: 0.9889 真实标签: 3, 预测标签: 3, 最大预测概率: 0.8773 真实标签: 4, 预测标签: 4, 最大预测概率: 0.9900 训练集准确率: 0.9469166666666666 测试集准确率: 0.9459 训练集混淆矩阵: [[5802 1 5 6 8 5 37 0 52 7] [ 0 6620 22 36 16 2 4 5 20 17] [ 53 45 5486 114 54 3 54 31 103 15] [ 21 22 51 5788 8 44 19 38 83 57] [ 9 6 10 2 5439 0 71 7 9 289] [ 42 29 11 114 41 4922 72 4 102 84] [ 35 14 5 1 10 20 5789 0 43 1] [ 8 50 53 35 52 3 3 5762 21 278] [ 14 75 11 76 25 5 41 1 5535 68] [ 20 7 3 72 90 11 4 32 38 5672]] 每个数字的精确率和召回率: 数字: 0, 精确率: 0.9796, 召回率: 0.9664 数字: 1, 精确率: 0.9819, 召回率: 0.9638 数字: 2, 精确率: 0.9208, 召回率: 0.9698 数字: 3, 精确率: 0.9441, 召回率: 0.9270 数字: 4, 精确率: 0.9310, 召回率: 0.9471 数字: 5, 精确率: 0.9080, 召回率: 0.9815 数字: 6, 精确率: 0.9782, 召回率: 0.9499 数字: 7, 精确率: 0.9197, 召回率: 0.9799 数字: 8, 精确率: 0.9460, 召回率: 0.9216 数字: 9, 精确率: 0.9534, 召回率: 0.8742
USING MNIST
The MNIST database (Modified National Institute of
Standards and Technology database) of handwritten
digits consists of a training set of 60,000 examples,
and a test set of 10,000 examples. It is a subset of a
larger set available from NIST. Additionally, the
black and white images from NIST were size-
normalized and centered to fit into a 28x28 pixel
bounding box and anti-aliased, which introduced
grayscale levels.
This database is well liked for training and testing in
the field of machine learning and image processing.
It is a remixed subset of the original NIST datasets.
One half of the 60,000 training images consist of
images from NIST's testing dataset and the other half
from Nist's training set. The 10,000 images from the
testing set are similarly assembled.
The MNIST dataset is used by researchers to test and
compare their research results with others. The
lowest error rates in literature are as low as 0.21
percent.1
READING THE MNIST DATA SET
The images from the data set have the size 28 x 28. They are saved in the csv data files mnist_train.csv and
mnist_test.csv.
Every line of these files consists of an image, i.e. 785 numbers between 0 and 255.
The first number of each line is the label, i.e. the digit which is depicted in the image. The following 784
numbers are the pixels of the 28 x 28 image.
import numpy as np
198
import matplotlib.pyplot as plt
image_size = 28 # width and length
no_of_different_labels = 10 # i.e. 0, 1, 2, 3, ..., 9
image_pixels = image_size * image_size
data_path = "data/mnist/"
train_data = np.loadtxt(data_path + "mnist_train.csv",
delimiter=",")
test_data = np.loadtxt(data_path + "mnist_test.csv",
delimiter=",")
test_data[:10]
Output:array([[7., 0., 0., ..., 0., 0., 0.],
[2., 0., 0., ..., 0., 0., 0.],
[1., 0., 0., ..., 0., 0., 0.],
...,
[9., 0., 0., ..., 0., 0., 0.],
[5., 0., 0., ..., 0., 0., 0.],
[9., 0., 0., ..., 0., 0., 0.]])
test_data[test_data==255]
test_data.shape
Output10000, 785)
The images of the MNIST dataset are greyscale and the pixels range between 0 and 255 including both
bounding values. We will map these values into an interval from [0.01, 1] by multiplying each pixel by 0.99 /
255 and adding 0.01 to the result. This way, we avoid 0 values as inputs, which are capable of preventing
weight updates, as we we seen in the introductory chapter.
fac = 0.99 / 255
train_imgs = np.asfarray(train_data[:, 1:]) * fac + 0.01
test_imgs = np.asfarray(test_data[:, 1:]) * fac + 0.01
train_labels = np.asfarray(train_data[:, :1])
test_labels = np.asfarray(test_data[:, :1])
We need the labels in our calculations in a one-hot representation. We have 10 digits from 0 to 9, i.e. lr =
np.arange(10).
Turning a label into one-hot representation can be achieved with the command: (lr==label).astype(np.int)
We demonstrate this in the following:
import numpy as np
199
lr = np.arange(10)
for label in range(10):
one_hot = (lr==label).astype(np.int)
print("label: ", label, " in one-hot representation: ", one_ho
t)
label:
0
in one-hot representation:
[1 0 0 0 0 0 0 0 0 0]
label:
1
in one-hot representation:
[0 1 0 0 0 0 0 0 0 0]
label:
2
in one-hot representation:
[0 0 1 0 0 0 0 0 0 0]
label:
3
in one-hot representation:
[0 0 0 1 0 0 0 0 0 0]
label:
4
in one-hot representation:
[0 0 0 0 1 0 0 0 0 0]
label:
5
in one-hot representation:
[0 0 0 0 0 1 0 0 0 0]
label:
6
in one-hot representation:
[0 0 0 0 0 0 1 0 0 0]
label:
7
in one-hot representation:
[0 0 0 0 0 0 0 1 0 0]
label:
8
in one-hot representation:
[0 0 0 0 0 0 0 0 1 0]
label:
9
in one-hot representation:
[0 0 0 0 0 0 0 0 0 1]
We are ready now to turn our labelled images into one-hot representations. Instead of zeroes and one, we
create 0.01 and 0.99, which will be better for our calculations:
lr = np.arange(no_of_different_labels)
# transform labels into one hot representation
train_labels_one_hot = (lr==train_labels).astype(np.float)
test_labels_one_hot = (lr==test_labels).astype(np.float)
# we don't want zeroes and ones in the labels neither:
train_labels_one_hot[train_labels_one_hot==0] = 0.01
train_labels_one_hot[train_labels_one_hot==1] = 0.99
test_labels_one_hot[test_labels_one_hot==0] = 0.01
test_labels_one_hot[test_labels_one_hot==1] = 0.99
Before we start using the MNIST data sets with our neural network, we will have a look at some images:
for i in range(10):
img = train_imgs[i].reshape((28,28))
plt.imshow(img, cmap="Greys")
plt.show()
200
201
202
203
DUMPING THE DATA FOR FASTER RELOAD
You may have noticed that it is quite slow to read in the data from the csv files.
We will save the data in binary format with the dump function from the pickle module:
import pickle
with open("data/mnist/pickled_mnist.pkl", "bw") as fh:
data = (train_imgs,
test_imgs,
train_labels,
test_labels,
train_labels_one_hot,
test_labels_one_hot)
pickle.dump(data, fh)
We are able now to read in the data by using pickle.load. This is a lot faster than using loadtxt on the csv files:
import pickle
with open("data/mnist/pickled_mnist.pkl", "br") as fh:
data = pickle.load(fh)
train_imgs = data[0]
204
test_imgs = data[1]
train_labels = data[2]
test_labels = data[3]
train_labels_one_hot = data[4]
test_labels_one_hot = data[5]
image_size = 28 # width and length
no_of_different_labels = 10 # i.e. 0, 1, 2, 3, ..., 9
image_pixels = image_size * image_size
CLASSIFYING THE DATA
We will use the following neuronal network class for our first classification:
import numpy as np
@np.vectorize
def sigmoid(x):
return 1 / (1 + np.e ** -x)
activation_function = sigmoid
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm((low - mean) / sd,
(upp - mean) / sd,
loc=mean,
scale=sd)
class NeuralNetwork:
def __init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_out_nodes = no_of_out_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.learning_rate = learning_rate
205
self.create_weight_matrices()
def create_weight_matrices(self):
"""
A method to initialize the weight
matrices of the neural network
"""
rad = 1 / np.sqrt(self.no_of_in_nodes)
X = truncated_normal(mean=0,
sd=1,
low=-rad,
upp=rad)
self.wih = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes))
rad = 1 / np.sqrt(self.no_of_hidden_nodes)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.who = X.rvs((self.no_of_out_nodes,
self.no_of_hidden_nodes))
def train(self, input_vector, target_vector):
"""
input_vector and target_vector can
be tuple, list or ndarray
"""
input_vector = np.array(input_vector, ndmin=2).T
target_vector = np.array(target_vector, ndmin=2).T
output_vector1 = np.dot(self.wih,
input_vector)
output_hidden = activation_function(output_vector1)
output_vector2 = np.dot(self.who,
output_hidden)
output_network = activation_function(output_vector2)
output_errors = target_vector - output_network
# update the weights:
tmp = output_errors * output_network \
* (1.0 - output_network)
tmp = self.learning_rate * np.dot(tmp,
output_hidden.T)
self.who += tmp
206
# calculate hidden errors:
hidden_errors = np.dot(self.who.T,
output_errors)
# update the weights:
tmp = hidden_errors * output_hidden * \
(1.0 - output_hidden)
self.wih += self.learning_rate \
* np.dot(tmp, input_vector.T)
def run(self, input_vector):
# input_vector can be tuple, list or ndarray
input_vector = np.array(input_vector, ndmin=2).T
output_vector = np.dot(self.wih,
input_vector)
output_vector = activation_function(output_vector)
output_vector = np.dot(self.who,
output_vector)
output_vector = activation_function(output_vector)
return output_vector
def confusion_matrix(self, data_array, labels):
cm = np.zeros((10, 10), int)
for i in range(len(data_array)):
res = self.run(data_array[i])
res_max = res.argmax()
target = labels[i][0]
cm[res_max, int(target)] += 1
return cm
def precision(self, label, confusion_matrix):
col = confusion_matrix[:, label]
return confusion_matrix[label, label] / col.sum()
def recall(self, label, confusion_matrix):
row = confusion_matrix[label, :]
return confusion_matrix[label, label] / row.sum()
207
def evaluate(self, data, labels):
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
res_max = res.argmax()
if res_max == labels[i]:
corrects += 1
else:
wrongs += 1
return corrects, wrongs
ANN = NeuralNetwork(no_of_in_nodes = image_pixels,
no_of_out_nodes = 10,
no_of_hidden_nodes = 100,
learning_rate = 0.1)
for i in range(len(train_imgs)):
ANN.train(train_imgs[i], train_labels_one_hot[i])
for i in range(20):
res = ANN.run(test_imgs[i])
print(test_labels[i], np.argmax(res), np.max(res))
[7.] 7 0.9829245583409039
[2.] 2 0.7372766887508578
[1.] 1 0.9881823673106839
[0.] 0 0.9873289971465894
[4.] 4 0.9456335245615916
[1.] 1 0.9880120617106172
[4.] 4 0.976550583573903
[9.] 9 0.964909168118122
[5.] 6 0.36615932726182665
[9.] 9 0.9848677489827125
[0.] 0 0.9204097234781773
[6.] 6 0.8897871402453337
[9.] 9 0.9936811621891628
[0.] 0 0.9832119513084644
[1.] 1 0.988750833073612
[5.] 5 0.9156741221523511
[9.] 9 0.9812577974620423
[7.] 7 0.9888560485875889
[3.] 3 0.8772868556722897
[4.] 4 0.9900030761222965
208
corrects, wrongs = ANN.evaluate(train_imgs, train_labels)
print("accuracy train: ", corrects / ( corrects + wrongs))
corrects, wrongs = ANN.evaluate(test_imgs, test_labels)
print("accuracy: test", corrects / ( corrects + wrongs))
cm = ANN.confusion_matrix(train_imgs, train_labels)
print(cm)
for i in range(10):
print("digit: ", i, "precision: ", ANN.precision(i, cm), "reca
ll: ", ANN.recall(i, cm))
accuracy train: 0.9469166666666666
accuracy: test 0.9459
[[5802
0
53
21
9
42
35
8
14
20]
[
1 6620
45
22
6
29
14
50
75
7]
[
5
22 5486
51
10
11
5
53
11
3]
[
6
36 114 5788
2 114
1
35
76
72]
[
8
16
54
8 5439
41
10
52
25
90]
[
5
2
3
44
0 4922
20
3
5
11]
[ 37
4
54
19
71
72 5789
3
41
4]
[
0
5
31
38
7
4
0 5762
1
32]
[ 52
20 103
83
9 102
43
21 5535
38]
[
7
17
15
57 289
84
1 278
68 5672]]
digit: 0 precision: 0.9795711632618606 recall: 0.96635576282478
35
digit: 1 precision: 0.9819044793829724 recall: 0.96375018197699
81
digit: 2 precision: 0.9207787848271232 recall: 0.96977196393848
33
digit: 3 precision: 0.9440548034578372 recall: 0.92696989109545
16
digit: 4 precision: 0.9310167750770284 recall: 0.94706599338324
91
digit: 5 precision: 0.9079505626268216 recall: 0.98145563310069
79
digit: 6 precision: 0.978202095302467 recall: 0.949950771250410
3
digit: 7 precision: 0.9197126895450918 recall: 0.97993197278911
57
digit: 8 precision: 0.945992138096052 recall: 0.921578421578421
6
digit: 9 precision: 0.953437552529837 recall: 0.87422934648582 -
重复训练:Epochs
我们可以重复训练多次。每次完整的训练循环被称为一个 “epoch”(或“训练轮次”)。
Pythonimport numpy as np # 假设之前的 NeuralNetwork 类、sigmoid 和 truncated_normal 函数已定义并可用 # 导入之前保存的数据 import pickle data_path = "data/mnist/" # 确保路径正确 with open(data_path + "pickled_mnist.pkl", "br") as fh: data = pickle.load(fh) train_imgs = data[0] test_imgs = data[1] train_labels = data[2] test_labels = data[3] train_labels_one_hot = data[4] test_labels_one_hot = data[5] image_size = 28 image_pixels = image_size * image_size no_of_different_labels = 10 # 激活函数和权重初始化辅助函数 @np.vectorize def sigmoid(x): return 1 / (1 + np.e ** -x) activation_function = sigmoid from scipy.stats import truncnorm def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm((low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) # 定义 NeuralNetwork 类 class NeuralNetwork: def __init__(self, no_of_in_nodes, no_of_out_nodes, no_of_hidden_nodes, learning_rate): self.no_of_in_nodes = no_of_in_nodes self.no_of_out_nodes = no_of_out_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.learning_rate = learning_rate self.create_weight_matrices() def create_weight_matrices(self): rad = 1 / np.sqrt(self.no_of_in_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.wih = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes)) rad = 1 / np.sqrt(self.no_of_hidden_nodes) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.who = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes)) def train_single(self, input_vector, target_vector): """ input_vector 和 target_vector 可以是元组、列表或 ndarray。 这个方法执行一次前向传播和一次反向传播(单个样本)。 """ input_vector = np.array(input_vector, ndmin=2).T target_vector = np.array(target_vector, ndmin=2).T # 前向传播 output_vector1 = np.dot(self.wih, input_vector) output_hidden = activation_function(output_vector1) output_vector2 = np.dot(self.who, output_hidden) output_network = activation_function(output_vector2) # 计算误差 output_errors = target_vector - output_network # 更新隐藏层到输出层的权重 (who) tmp = output_errors * output_network * (1.0 - output_network) tmp = self.learning_rate * np.dot(tmp, output_hidden.T) self.who += tmp # 计算隐藏层误差 hidden_errors = np.dot(self.who.T, output_errors) # 更新输入层到隐藏层的权重 (wih) tmp = hidden_errors * output_hidden * (1.0 - output_hidden) self.wih += self.learning_rate * np.dot(tmp, input_vector.T) def train(self, data_array, labels_one_hot_array, epochs=1, intermediate_results=False): """ 这个方法在整个数据集上重复训练多个 epoch。 如果 intermediate_results 为 True,则返回每个 epoch 后的权重。 """ intermediate_weights = [] for epoch in range(epochs): print(f"Epoch: {epoch+1}/{epochs}", end="\r") # 打印当前 epoch 进度 for i in range(len(data_array)): self.train_single(data_array[i], labels_one_hot_array[i]) # 在每个 epoch 结束时,评估并打印准确率 corrects, wrongs = self.evaluate(train_imgs, train_labels) train_accuracy = corrects / (corrects + wrongs) corrects, wrongs = self.evaluate(test_imgs, test_labels) test_accuracy = corrects / (corrects + wrongs) print(f"Epoch: {epoch+1}/{epochs} - 训练准确率: {train_accuracy:.4f}, 测试准确率: {test_accuracy:.4f}") if intermediate_results: intermediate_weights.append((self.wih.copy(), self.who.copy())) return intermediate_weights if intermediate_results else None def confusion_matrix(self, data_array, labels): cm = {} # 使用字典存储,因为矩阵可能稀疏 for i in range(len(data_array)): res = self.run(data_array[i]) res_max = res.argmax() target = labels[i][0] # 真实的标签 # 将浮点型标签转换为整数,作为字典的键 key = (int(target), res_max) cm[key] = cm.get(key, 0) + 1 # 增加计数 return cm def run(self, input_vector): input_vector = np.array(input_vector, ndmin=2).T output_vector = np.dot(self.wih, input_vector) output_vector = activation_function(output_vector) output_vector = np.dot(self.who, output_vector) output_vector = activation_function(output_vector) return output_vector def evaluate(self, data, labels): corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() # 确保标签是整数进行比较 if res_max == int(labels[i][0]): corrects += 1 else: wrongs += 1 return corrects, wrongs # --- 运行训练 --- epochs_to_run = 3 # 设定训练轮次 NN = NeuralNetwork(no_of_in_nodes=image_pixels, no_of_out_nodes=10, no_of_hidden_nodes=100, learning_rate=0.1) print("开始多轮训练...") # 直接调用新的 train 方法,它会打印每个 epoch 的准确率 NN.train(train_imgs, train_labels_one_hot, epochs=epochs_to_run, intermediate_results=False) # 这里不需要存储中间权重 print("\n多轮训练完成。") # 再次评估最终准确率(通常会比 epoch 结束时的最后一次打印的更准确,因为训练是连续的) corrects_train_final, wrongs_train_final = NN.evaluate(train_imgs, train_labels) print(f"最终训练准确率: {corrects_train_final / (corrects_train_final + wrongs_train_final):.4f}") corrects_test_final, wrongs_test_final = NN.evaluate(test_imgs, test_labels) print(f"最终测试准确率: {corrects_test_final / (corrects_test_final + wrongs_test_final):.4f}")
输出示例(每次运行可能略有不同,但趋势应是准确率逐渐提高):
开始多轮训练... Epoch: 1/3 - 训练准确率: 0.9452, 测试准确率: 0.9459 Epoch: 2/3 - 训练准确率: 0.9627, 测试准确率: 0.9582 Epoch: 3/3 - 训练准确率: 0.9699, 测试准确率: 0.9626 多轮训练完成。 最终训练准确率: 0.9699 最终测试准确率: 0.9626
为了重复训练,我们对
NeuralNetwork
类进行了以下修改:-
train_single
方法:这个方法基本上就是之前被称为train
的逻辑,它负责对单个输入-目标对执行一次前向传播和反向传播以更新权重。 -
新的
train
方法:这个方法现在负责管理训练的“epoch”计数。它会循环执行指定次数的 epoch,在每个 epoch 内遍历整个训练数据集,并调用train_single
方法来更新权重。 -
中间结果存储:为了测试目的,我们增加了
intermediate_results
参数。如果设置为True
,它会在每个 epoch 结束后,将当前的权重矩阵self.wih
和self.who
的副本保存到intermediate_weights
列表中并返回。这对于分析训练过程中模型性能的变化非常有用。 -
混淆矩阵字典:为了更好地处理可能稀疏的混淆矩阵,
confusion_matrix
方法现在使用字典来存储 (实际标签, 预测标签) 的计数,而不是固定的 NumPy 数组。这样可以更灵活地处理各种标签组合,并且对于那些从未出现过的错误分类,它不会在内存中占用空间。 -
评估标签类型:在
evaluate
和confusion_matrix
方法中,我们确保在与res_max
(预测的整数索引)比较或作为字典键使用时,真实的标签也转换为整数类型(例如int(labels[i][0])
)。
通过这些改进,我们能够更清晰地组织训练过程,并更好地观察神经网络在多个训练轮次中的性能提升。您可以看到,随着 epoch 的增加,训练集和测试集的准确率都在稳步提升,这表明网络正在有效地从数据中学习。
We can repeat the training multiple times. Each run is called an "epoch".
epochs = 3
NN = NeuralNetwork(no_of_in_nodes = image_pixels,
no_of_out_nodes = 10,
no_of_hidden_nodes = 100,
learning_rate = 0.1)
for epoch in range(epochs):
print("epoch: ", epoch)
for i in range(len(train_imgs)):
NN.train(train_imgs[i],
train_labels_one_hot[i])
corrects, wrongs = NN.evaluate(train_imgs, train_labels)
print("accuracy train: ", corrects / ( corrects + wrongs))
corrects, wrongs = NN.evaluate(test_imgs, test_labels)
print("accuracy: test", corrects / ( corrects + wrongs))
epoch: 0
accruracy train: 0.94515
accruracy: test 0.9459
epoch: 1
accruracy train: 0.9626833333333333
accruracy: test 0.9582
epoch: 2
accruracy train: 0.96995
accruracy: test 0.9626
We want to do the multiple training of the training set inside of our network. To this purpose we rewrite the
method train and add a method train_single. train_single is more or less what we called 'train' before. Whereas
the new 'train' method is doing the epoch counting. For testing purposes, we save the weight matrices after
each epoch in
the list intermediate_weights. This list is returned as the output of train:
import numpy as np
@np.vectorize
def sigmoid(x):
210
return 1 / (1 + np.e ** -x)
activation_function = sigmoid
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm((low - mean) / sd,
(upp - mean) / sd,
loc=mean,
scale=sd)
class NeuralNetwork:
def __init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_out_nodes = no_of_out_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.learning_rate = learning_rate
self.create_weight_matrices()
def create_weight_matrices(self):
""" A method to initialize the weight matrices of the neur
al network"""
rad = 1 / np.sqrt(self.no_of_in_nodes)
X = truncated_normal(mean=0,
sd=1,
low=-rad,
upp=rad)
self.wih = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes))
rad = 1 / np.sqrt(self.no_of_hidden_nodes)
X = truncated_normal(mean=0,
sd=1,
low=-rad,
upp=rad)
self.who = X.rvs((self.no_of_out_nodes,
self.no_of_hidden_nodes))
def train_single(self, input_vector, target_vector):
211
n)
r.T)
def"""
input_vector and target_vector can be tuple,
list or ndarray
"""
output_vectors = []
input_vector = np.array(input_vector, ndmin=2).T
target_vector = np.array(target_vector, ndmin=2).T
output_vector1 = np.dot(self.wih,
input_vector)
output_hidden = activation_function(output_vector1)
output_vector2 = np.dot(self.who,
output_hidden)
output_network = activation_function(output_vector2)
output_errors = target_vector - output_network
# update the weights:
tmp = output_errors * output_network * \
(1.0 - output_network)
tmp = self.learning_rate * np.dot(tmp,
output_hidden.T)
self.who += tmp
# calculate hidden errors:
hidden_errors = np.dot(self.who.T,
output_errors)
# update the weights:
tmp = hidden_errors * output_hidden * (1.0 - output_hidde
self.wih += self.learning_rate * np.dot(tmp, input_vecto
train(self, data_array,
labels_one_hot_array,
epochs=1,
intermediate_results=False):
intermediate_weights = []
for epoch in range(epochs):
print("*", end="")
for i in range(len(data_array)):
212
ifreturnself.train_single(data_array[i],
labels_one_hot_array[i])
intermediate_results:
intermediate_weights.append((self.wih.copy(),
self.who.copy()))
intermediate_weights
def confusion_matrix(self, data_array, labels):
cm = {}
for i in range(len(data_array)):
res = self.run(data_array[i])
res_max = res.argmax()
target = labels[i][0]
if (target, res_max) in cm:
cm[(target, res_max)] += 1
else:
cm[(target, res_max)] = 1
return cm
def run(self, input_vector):
""" input_vector can be tuple, list or ndarray """
input_vector = np.array(input_vector, ndmin=2).T
output_vector = np.dot(self.wih,
input_vector)
output_vector = activation_function(output_vector)
output_vector = np.dot(self.who,
output_vector)
output_vector = activation_function(output_vector)
return output_vector
def evaluate(self, data, labels):
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
res_max = res.argmax()
if res_max == labels[i]:
corrects += 1
else:
wrongs += 1
return corrects, wrongs
213
epochs = 10
ANN = NeuralNetwork(no_of_in_nodes = image_pixels,
no_of_out_nodes = 10,
no_of_hidden_nodes = 100,
learning_rate = 0.15)
weights = ANN.train(train_imgs,
train_labels_one_hot,
epochs=epochs,
intermediate_results=True)
**********
cm = ANN.confusion_matrix(train_imgs, train_labels)
print(ANN.run(train_imgs[i]))
[[2.60149245e-03]
[2.52542556e-03]
[6.57990628e-03]
[1.32663729e-03]
[1.34985384e-03]
[2.63840265e-04]
[2.18329159e-04]
[1.32693720e-04]
[9.84326084e-01]
[4.34559417e-02]]
cm = list(cm.items())
print(sorted(cm))
214
[((0.0, 0), 5853), ((0.0, 1), 1), ((0.0, 2), 3), ((0.0, 4), 8),
((0.0, 5), 2), ((0.0, 6), 12), ((0.0, 7), 7), ((0.0, 8), 27),
((0.0, 9), 10), ((1.0, 0), 1), ((1.0, 1), 6674), ((1.0, 2), 17),
((1.0, 3), 5), ((1.0, 4), 14), ((1.0, 5), 2), ((1.0, 6), 1),
((1.0, 7), 6), ((1.0, 8), 15), ((1.0, 9), 7), ((2.0, 0), 37),
((2.0, 1), 14), ((2.0, 2), 5791), ((2.0, 3), 17), ((2.0, 4), 11),
((2.0, 5), 2), ((2.0, 6), 10), ((2.0, 7), 15), ((2.0, 8), 51),
((2.0, 9), 10), ((3.0, 0), 16), ((3.0, 1), 5), ((3.0, 2), 34),
((3.0, 3), 5869), ((3.0, 4), 8), ((3.0, 5), 57), ((3.0, 6), 4),
((3.0, 7), 20), ((3.0, 8), 58), ((3.0, 9), 60), ((4.0, 0), 14),
((4.0, 1), 6), ((4.0, 2), 8), ((4.0, 3), 1), ((4.0, 4), 5678),
((4.0, 5), 1), ((4.0, 6), 14), ((4.0, 7), 5), ((4.0, 8), 11),
((4.0, 9), 104), ((5.0, 0), 7), ((5.0, 1), 2), ((5.0, 2), 6),
((5.0, 3), 27), ((5.0, 4), 5), ((5.0, 5), 5312), ((5.0, 6), 12),
((5.0, 7), 5), ((5.0, 8), 20), ((5.0, 9), 25), ((6.0, 0), 32),
((6.0, 1), 5), ((6.0, 2), 1), ((6.0, 4), 10), ((6.0, 5), 52),
((6.0, 6), 5791), ((6.0, 8), 26), ((6.0, 9), 1), ((7.0, 0), 5),
((7.0, 1), 11), ((7.0, 2), 22), ((7.0, 3), 2), ((7.0, 4), 17),
((7.0, 5), 3), ((7.0, 6), 2), ((7.0, 7), 6074), ((7.0, 8), 26),
((7.0, 9), 103), ((8.0, 0), 20), ((8.0, 1), 18), ((8.0, 2), 9),
((8.0, 3), 14), ((8.0, 4), 27), ((8.0, 5), 24), ((8.0, 6), 9),
((8.0, 7), 8), ((8.0, 8), 5668), ((8.0, 9), 54), ((9.0, 0), 26),
((9.0, 1), 2), ((9.0, 2), 2), ((9.0, 3), 16), ((9.0, 4), 69),
((9.0, 5), 14), ((9.0, 6), 7), ((9.0, 7), 19), ((9.0, 8), 15),
((9.0, 9), 5779)]
In [ ]:
for i in range(epochs):
print("epoch: ", i)
ANN.wih = weights[i][0]
ANN.who = weights[i][1]
corrects, wrongs = ANN.evaluate(train_imgs, train_labels)
print("accuracy train: ", corrects / ( corrects + wrongs))
corrects, wrongs = ANN.evaluate(test_imgs, test_labels)
print("accuracy: test", corrects / ( corrects + wrongs)) -
-
Pythonimport numpy as np # 激活函数:Sigmoid @np.vectorize def sigmoid(x): return 1 / (1 + np.e ** -x) activation_function = sigmoid # 截断正态分布,用于初始化权重 from scipy.stats import truncnorm def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm((low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) # --- 神经网络类定义 --- class NeuralNetwork: def __init__(self, no_of_in_nodes, # 输入层节点数 no_of_out_nodes, # 输出层节点数 no_of_hidden_nodes,# 隐藏层节点数 learning_rate, # 学习率 bias=None # 偏置项,如果为None则不使用偏置 ): self.no_of_in_nodes = no_of_in_nodes self.no_of_out_nodes = no_of_out_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.learning_rate = learning_rate self.bias = bias # 偏置值,例如 0.5 或 1.0 self.create_weight_matrices() def create_weight_matrices(self): """ 一个初始化神经网络权重矩阵的方法,支持可选的偏置节点。 """ bias_node = 1 if self.bias else 0 # 如果有偏置,则偏置节点数为1,否则为0 # 输入层到隐藏层的权重 (wih) 初始化 # 权重初始化范围取决于输入节点数(包括偏置节点) rad = 1 / np.sqrt(self.no_of_in_nodes + bias_node) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.wih = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes + bias_node)) # 隐藏层节点数 x (输入节点数 + 偏置节点数) # 隐藏层到输出层的权重 (who) 初始化 # 权重初始化范围取决于隐藏层节点数(包括偏置节点) rad = 1 / np.sqrt(self.no_of_hidden_nodes + bias_node) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.who = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes + bias_node)) # 输出层节点数 x (隐藏层节点数 + 偏置节点数) def train(self, input_vector, target_vector): """ 训练方法:执行一次前向传播和一次反向传播。 input_vector 和 target_vector 可以是元组、列表或 ndarray。 """ # 如果使用偏置,将偏置节点添加到输入向量的末尾 if self.bias: input_vector = np.concatenate((input_vector, [self.bias])) # 将输入和目标向量转换为列向量 input_vector = np.array(input_vector, ndmin=2).T target_vector = np.array(target_vector, ndmin=2).T # 前向传播:从输入层到隐藏层 output_vector1 = np.dot(self.wih, input_vector) output_hidden = activation_function(output_vector1) # 如果使用偏置,将偏置节点添加到隐藏层输出的末尾 if self.bias: output_hidden = np.concatenate((output_hidden, [[self.bias]])) # 前向传播:从隐藏层到输出层 output_vector2 = np.dot(self.who, output_hidden) output_network = activation_function(output_vector2) # 计算输出误差 output_errors = target_vector - output_network # 更新隐藏层到输出层的权重 (who) tmp = output_errors * output_network * (1.0 - output_network) # 输出层的梯度 tmp = self.learning_rate * np.dot(tmp, output_hidden.T) self.who += tmp # 计算隐藏层误差(反向传播到隐藏层) hidden_errors = np.dot(self.who.T, output_errors) # 更新输入层到隐藏层的权重 (wih) tmp = hidden_errors * output_hidden * (1.0 - output_hidden) # 隐藏层的梯度 # 如果有偏置,去除偏置节点对应的梯度,因为偏置节点没有输入误差反向传播 if self.bias: x = np.dot(tmp, input_vector.T)[:-1, :] # 去除最后一列(偏置项) else: x = np.dot(tmp, input_vector.T) self.wih += self.learning_rate * x def run(self, input_vector): """ 运行方法:对给定输入执行前向传播以获得输出。 input_vector 可以是元组、列表或 ndarray。 """ # 如果使用偏置,将偏置节点添加到输入向量的末尾 if self.bias: input_vector = np.concatenate((input_vector, [self.bias])) # 注意这里偏置值用 self.bias input_vector = np.array(input_vector, ndmin=2).T # 前向传播:输入层到隐藏层 output_vector = np.dot(self.wih, input_vector) output_vector = activation_function(output_vector) # 如果使用偏置,将偏置节点添加到隐藏层输出的末尾 if self.bias: output_vector = np.concatenate((output_vector, [[self.bias]])) # 注意这里偏置值用 self.bias # 前向传播:隐藏层到输出层 output_vector = np.dot(self.who, output_vector) output_vector = activation_function(output_vector) return output_vector def evaluate(self, data, labels): """ 评估网络在给定数据集上的表现。 """ corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() # 预测结果的索引(即预测的数字) if res_max == int(labels[i][0]): # 将真实标签转换为整数进行比较 corrects += 1 else: wrongs += 1 return corrects, wrongs # --- 训练和测试(无偏置)--- # 导入之前保存的数据 (假设已经运行了前一部分代码并保存了数据) import pickle data_path = "data/mnist/" try: with open(data_path + "pickled_mnist.pkl", "br") as fh: data = pickle.load(fh) train_imgs = data[0] test_imgs = data[1] train_labels = data[2] test_labels = data[3] train_labels_one_hot = data[4] test_labels_one_hot = data[5] image_size = 28 image_pixels = image_size * image_size no_of_different_labels = 10 except FileNotFoundError: print("MNIST 数据文件未找到。请先运行前面部分的代码以生成 'pickled_mnist.pkl'。") exit() print("--- 无偏置项的神经网络训练 ---") ANN = NeuralNetwork(no_of_in_nodes=image_pixels, no_of_out_nodes=10, no_of_hidden_nodes=200, # 隐藏层节点数增加到200 learning_rate=0.1, bias=None) # 不使用偏置 # 单次训练循环(遍历所有训练样本一次) for i in range(len(train_imgs)): ANN.train(train_imgs[i], train_labels_one_hot[i]) print("测试集前20个样本的预测结果:") for i in range(20): res = ANN.run(test_imgs[i]) print(f"真实标签: {int(test_labels[i][0])}, 预测标签: {np.argmax(res)}, 最大预测概率: {np.max(res):.4f}") corrects_train, wrongs_train = ANN.evaluate(train_imgs, train_labels) print("训练准确率: ", corrects_train / (corrects_train + wrongs_train)) corrects_test, wrongs_test = ANN.evaluate(test_imgs, test_labels) print("测试准确率: ", corrects_test / (corrects_test + wrongs_test)) print("\n--- 带偏置项和 Epochs 的神经网络训练 ---") # --- 带有偏置项和 Epochs 的版本 --- class NeuralNetwork: # 重新定义类,因为之前的示例只是更改了train方法,这里为了完整性重新包含整个类 def __init__(self, no_of_in_nodes, no_of_out_nodes, no_of_hidden_nodes, learning_rate, bias=None ): self.no_of_in_nodes = no_of_in_nodes self.no_of_out_nodes = no_of_out_nodes self.no_of_hidden_nodes = no_of_hidden_nodes self.learning_rate = learning_rate self.bias = bias self.create_weight_matrices() def create_weight_matrices(self): bias_node = 1 if self.bias else 0 rad = 1 / np.sqrt(self.no_of_in_nodes + bias_node) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.wih = X.rvs((self.no_of_hidden_nodes, self.no_of_in_nodes + bias_node)) rad = 1 / np.sqrt(self.no_of_hidden_nodes + bias_node) X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad) self.who = X.rvs((self.no_of_out_nodes, self.no_of_hidden_nodes + bias_node)) def train_single(self, input_vector, target_vector): """ 单样本训练,带有偏置项处理。 """ # 如果使用偏置,将偏置节点添加到输入向量的末尾 if self.bias: input_vector = np.concatenate((input_vector, [self.bias])) input_vector = np.array(input_vector, ndmin=2).T target_vector = np.array(target_vector, ndmin=2).T output_vector1 = np.dot(self.wih, input_vector) output_hidden = activation_function(output_vector1) if self.bias: output_hidden = np.concatenate((output_hidden, [[self.bias]])) output_vector2 = np.dot(self.who, output_hidden) output_network = activation_function(output_vector2) output_errors = target_vector - output_network tmp = output_errors * output_network * (1.0 - output_network) tmp = self.learning_rate * np.dot(tmp, output_hidden.T) self.who += tmp hidden_errors = np.dot(self.who.T, output_errors) tmp = hidden_errors * output_hidden * (1.0 - output_hidden) if self.bias: x = np.dot(tmp, input_vector.T)[:-1, :] else: x = np.dot(tmp, input_vector.T) self.wih += self.learning_rate * x def train(self, data_array, labels_one_hot_array, epochs=1, intermediate_results=False): """ 多 epoch 训练,可以保存中间权重。 """ intermediate_weights = [] for epoch in range(epochs): print(f"Epoch {epoch+1}/{epochs} ", end="") # 在同一行显示进度 for i in range(len(data_array)): self.train_single(data_array[i], labels_one_hot_array[i]) # 在每个 epoch 结束时,评估并打印准确率 corrects, wrongs = self.evaluate(train_imgs, train_labels) train_accuracy = corrects / (corrects + wrongs) corrects, wrongs = self.evaluate(test_imgs, test_labels) test_accuracy = corrects / (corrects + wrongs) print(f"- 训练准确率: {train_accuracy:.4f}, 测试准确率: {test_accuracy:.4f}") if intermediate_results: intermediate_weights.append((self.wih.copy(), self.who.copy())) return intermediate_weights def run(self, input_vector): """ 运行方法,带有偏置项处理。 """ if self.bias: input_vector = np.concatenate((input_vector, [self.bias])) input_vector = np.array(input_vector, ndmin=2).T output_vector = np.dot(self.wih, input_vector) output_vector = activation_function(output_vector) if self.bias: output_vector = np.concatenate((output_vector, [[self.bias]])) output_vector = np.dot(self.who, output_vector) output_vector = activation_function(output_vector) return output_vector def evaluate(self, data, labels): """ 评估网络在给定数据集上的表现。 """ corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() if res_max == int(labels[i][0]): # 确保标签是整数 corrects += 1 else: wrongs += 1 return corrects, wrongs # --- 带有偏置项的训练示例 --- epochs_with_bias = 12 network = NeuralNetwork(no_of_in_nodes=image_pixels, no_of_out_nodes=10, no_of_hidden_nodes=100, # 隐藏层节点数,这里使用100 learning_rate=0.1, bias=0.5) # 使用偏置项,值为 0.5 print(f"\n使用 {epochs_with_bias} 个 epoch 训练神经网络 (包含偏置项):") weights = network.train(train_imgs, train_labels_one_hot, epochs=epochs_with_bias, intermediate_results=True) # 打印每个 epoch 的准确率 print("\n每个 epoch 的训练和测试准确率:") for epoch in range(epochs_with_bias): print(f"epoch: {epoch}") # 恢复该 epoch 结束时的权重 network.wih = weights[epoch][0] network.who = weights[epoch][1] corrects_train, wrongs_train = network.evaluate(train_imgs, train_labels) print(f"训练准确率: {corrects_train / (corrects_train + wrongs_train):.4f}") corrects_test, wrongs_test = network.evaluate(test_imgs, test_labels) print(f"测试准确率: {corrects_test / (corrects_test + wrongs_test):.4f}") print("\n--- 大规模参数搜索和结果保存(到nist_tests.csv)---") print("注意:此部分代码运行时间较长。") # 循环遍历不同参数组合进行训练和评估 # 确保 'nist_tests.csv' 文件可以写入 with open("nist_tests.csv", "w") as fh_out: for hidden_nodes in [20, 50, 100, 120, 150]: for learning_rate in [0.01, 0.05, 0.1, 0.2]: for bias_val in [None, 0.5]: # 注意这里我把变量名从 bias 改为 bias_val 以避免与 NeuralNetwork.bias 混淆 print(f"测试: hidden_nodes={hidden_nodes}, learning_rate={learning_rate}, bias={bias_val}") current_network = NeuralNetwork(no_of_in_nodes=image_pixels, no_of_out_nodes=10, no_of_hidden_nodes=hidden_nodes, learning_rate=learning_rate, bias=bias_val) current_weights = current_network.train(train_imgs, train_labels_one_hot, epochs=epochs_with_bias, # 使用与前面相同的 epoch 数量 intermediate_results=True) # 遍历每个 epoch 的结果并写入文件 for epoch_idx in range(epochs_with_bias): print("*", end="") # 打印星号表示进度 current_network.wih = current_weights[epoch_idx][0] current_network.who = current_weights[epoch_idx][1] train_corrects, train_wrongs = current_network.evaluate(train_imgs, train_labels) test_corrects, test_wrongs = current_network.evaluate(test_imgs, test_labels) # 格式化输出字符串 outstr = f"{hidden_nodes} {learning_rate} {bias_val} {epoch_idx} " outstr += f"{train_corrects / (train_corrects + train_wrongs):.6f} " # 训练准确率 outstr += f"{train_wrongs / (train_corrects + train_wrongs):.6f} " # 训练错误率 outstr += f"{test_corrects / (test_corrects + test_wrongs):.6f} " # 测试准确率 outstr += f"{test_wrongs / (test_corrects + test_wrongs):.6f}" # 测试错误率 fh_out.write(outstr + "\n") fh_out.flush() # 立即写入文件,防止数据丢失 print("") # 换行,以便下一个参数组合的输出 print("\n所有测试结果已写入 nist_tests.csv 文件。")
代码解析与改进:偏置项 (Bias) 的引入
这段代码对之前实现的神经网络进行了关键的改进:引入了偏置项 (bias)。偏置项允许神经网络在没有输入信号激活神经元的情况下,仍然可以激活输出。这增加了模型的灵活性和表达能力,使其能够更好地拟合数据。
偏置项是如何工作的?
-
__init__
方法中的bias
参数:-
self.bias = bias
:神经网络类现在可以在初始化时接受一个bias
参数(例如0.5
或1.0
)。如果bias
为None
,则不使用偏置。
-
-
create_weight_matrices
中的权重初始化:-
bias_node = 1 if self.bias else 0
:根据是否使用偏置,确定是否需要额外的偏置节点。 -
self.wih
和self.who
的维度:权重矩阵的列数现在包含了偏置节点。这意味着权重矩阵会多出一列,专门用于连接偏置节点。 -
rad = 1 / np.sqrt(self.no_of_in_nodes + bias_node)
和rad = 1 / np.sqrt(self.no_of_hidden_nodes + bias_node)
:权重初始化的范围也考虑了偏置节点的存在,以确保适当的缩放。
-
-
train
和train_single
方法中的偏置项处理:-
输入向量的拼接:
input_vector = np.concatenate((input_vector, [self.bias]))
。在每次训练或运行前,如果启用了偏置,会在输入向量的末尾拼接一个固定值 (self.bias
)。这个值就是偏置神经元的激活值,它总是为self.bias
。 -
隐藏层输出的拼接:
output_hidden = np.concatenate((output_hidden, [[self.bias]]))
。类似地,在隐藏层计算出输出后,如果启用了偏置,也会在其末尾拼接一个偏置节点的值,以便将其作为下一层(输出层)的输入。 -
权重更新的调整:在更新
self.wih
时,x = np.dot(tmp, input_vector.T)[:-1, :]
。这是因为从隐藏层反向传播回输入层的误差梯度需要排除偏置节点(因为偏置节点没有“上游”输入,其值是固定的,不需要根据误差进行调整)。
-
-
run
方法中的偏置项处理:-
与
train
类似,run
方法也会在处理输入向量和隐藏层输出时,根据self.bias
是否存在来拼接偏置值。
-
实验与结果
代码首先展示了一个不使用偏置项的单轮训练示例,然后展示了带有偏置项和多轮 (epochs) 训练的示例。
无偏置项的训练输出示例:
--- 无偏置项的神经网络训练 --- 测试集前20个样本的预测结果: 真实标签: 7, 预测标签: 7, 最大预测概率: 0.9951 ... 训练准确率: 0.9556 测试准确率: 0.9544
带有偏置项和多轮训练的输出示例:
--- 带偏置项和 Epochs 的神经网络训练 --- 使用 12 个 epoch 训练神经网络 (包含偏置项): Epoch 1/12 - 训练准确率: 0.9428, 测试准确率: 0.9415 Epoch 2/12 - 训练准确率: 0.9597, 测试准确率: 0.9548 Epoch 3/12 - 训练准确率: 0.9673, 测试准确率: 0.9599 Epoch 4/12 - 训练准确率: 0.9693, 测试准确率: 0.9601 Epoch 5/12 - 训练准确率: 0.9720, 测试准确率: 0.9631 Epoch 6/12 - 训练准确率: 0.9751, 测试准确率: 0.9659 Epoch 7/12 - 训练准确率: 0.9770, 测试准确率: 0.9662 Epoch 8/12 - 训练准确率: 0.9768, 测试准确率: 0.9644 Epoch 9/12 - 训练准确率: 0.9766, 测试准确率: 0.9643 Epoch 10/12 - 训练准确率: 0.9771, 测试准确率: 0.9643 Epoch 11/12 - 训练准确率: 0.9780, 测试准确率: 0.9627 Epoch 12/12 - 训练准确率: 0.9787, 测试准确率: 0.9638 每个 epoch 的训练和测试准确率: epoch: 0 训练准确率: 0.9428 测试准确率: 0.9415 ... (后续 epoch 的准确率)
从输出可以看出,引入偏置项后,神经网络的性能(准确率)得到了提升。例如,测试准确率从
0.9544
(无偏置) 提高到0.9638
(有偏置)。这证明了偏置项在增强神经网络建模能力方面的作用。大规模参数搜索
代码的最后一部分展示了一个大规模的参数搜索过程。它通过嵌套循环遍历了不同的超参数组合:
-
隐藏层节点数 (
no_of_hidden_nodes
):[20, 50, 100, 120, 150]
-
学习率 (
learning_rate
):[0.01, 0.05, 0.1, 0.2]
-
偏置项 (
bias
):[None, 0.5]
对于每种参数组合,神经网络都会训练
epochs
轮,并在每个 epoch 结束后记录训练和测试的准确率和错误率,将这些结果写入nist_tests.csv
文件。这个过程通常用于超参数调优 (hyperparameter tuning),以找到在特定数据集上表现最佳的模型配置。总结
这段代码通过引入偏置项,显著增强了神经网络的性能。同时,通过结构化的多 epoch 训练和大规模的参数搜索,展示了如何系统地评估和优化神经网络模型。这些技术是机器学习实践中至关重要的步骤,有助于构建更强大、更准确的分类器。
您对偏置项的工作原理,或者如何进一步分析
nist_tests.csv
文件中的结果,有什么想了解的吗?
import numpy as np
@np.vectorize
def sigmoid(x):
return 1 / (1 + np.e ** -x)
activation_function = sigmoid
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm((low - mean) / sd,
(upp - mean) / sd,
loc=mean,
scale=sd)
class NeuralNetwork:
def__init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate,
bias=None
):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_out_nodes = no_of_out_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.learning_rate = learning_rate
self.bias = bias
self.create_weight_matrices()
def create_weight_matrices(self):
"""
A method to initialize the weight
matrices of the neural network with
optional bias nodes
216
"""
bias_node = 1 if self.bias else 0
rad = 1 / np.sqrt(self.no_of_in_nodes + bias_node)
X = truncated_normal(mean=0,
sd=1,
low=-rad,
upp=rad)
self.wih = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes + bias_node))
rad = 1 / np.sqrt(self.no_of_hidden_nodes + bias_node)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.who = X.rvs((self.no_of_out_nodes,
self.no_of_hidden_nodes + bias_node))
def train(self, input_vector, target_vector):
"""
input_vector and target_vector can
be tuple, list or ndarray
"""
bias_node = 1 if self.bias else 0
if self.bias:
# adding bias node to the end of the inpuy_vector
input_vector = np.concatenate((input_vector,
[self.bias]) )
input_vector = np.array(input_vector, ndmin=2).T
target_vector = np.array(target_vector, ndmin=2).T
output_vector1 = np.dot(self.wih,
input_vector)
output_hidden = activation_function(output_vector1)
if self.bias:
output_hidden = np.concatenate((output_hidden,
[[self.bias]]) )
217
rk)
n)
defoutput_vector2 = np.dot(self.who,
output_hidden)
output_network = activation_function(output_vector2)
output_errors = target_vector - output_network
# update the weights:
tmp = output_errors * output_network * (1.0 - output_netwo
tmp = self.learning_rate
* np.dot(tmp, output_hidden.T)
self.who += tmp
# calculate hidden errors:
hidden_errors = np.dot(self.who.T,
output_errors)
# update the weights:
tmp = hidden_errors * output_hidden * (1.0 - output_hidde
if self.bias:
x = np.dot(tmp, input_vector.T)[:-1,:]
else:
x = np.dot(tmp, input_vector.T)
self.wih += self.learning_rate * x
run(self, input_vector):
"""
input_vector can be tuple, list or ndarray
"""
if self.bias:
# adding bias node to the end of the inpuy_vector
input_vector = np.concatenate((input_vector, [1]) )
input_vector = np.array(input_vector, ndmin=2).T
output_vector = np.dot(self.wih,
input_vector)
output_vector = activation_function(output_vector)
if self.bias:
output_vector = np.concatenate( (output_vector,
[[1]]) )
218
output_vector = np.dot(self.who,
output_vector)
output_vector = activation_function(output_vector)
return output_vector
def evaluate(self, data, labels):
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
res_max = res.argmax()
if res_max == labels[i]:
corrects += 1
else:
wrongs += 1
return corrects, wrongs
ANN = NeuralNetwork(no_of_in_nodes=image_pixels,
no_of_out_nodes=10,
no_of_hidden_nodes=200,
learning_rate=0.1,
bias=None)
for i in range(len(train_imgs)):
ANN.train(train_imgs[i], train_labels_one_hot[i])
for i in range(20):
res = ANN.run(test_imgs[i])
print(test_labels[i], np.argmax(res), np.max(res))
219
[7.] 7 0.9951478957895473
[2.] 2 0.9167137305226186
[1.] 1 0.9930670538508068
[0.] 0 0.9729093609525741
[4.] 4 0.9475097483176407
[1.] 1 0.9919906877733081
[4.] 4 0.9390079959736829
[9.] 9 0.9815469745110644
[5.] 5 0.23871278844097427
[9.] 9 0.9863859218561386
[0.] 0 0.9667234471027278
[6.] 6 0.8856024953669486
[9.] 9 0.9928943830319253
[0.] 0 0.96922568081586
[1.] 1 0.9899747475376088
[5.] 5 0.9595147911735664
[9.] 9 0.9958119066147573
[7.] 7 0.9883146384365381
[3.] 3 0.8706223167904136
[4.] 4 0.9912284156702522
corrects, wrongs = ANN.evaluate(train_imgs, train_labels)
print("accuracy train: ", corrects / ( corrects + wrongs))
corrects, wrongs = ANN.evaluate(test_imgs, test_labels)
print("accuracy: test", corrects / ( corrects + wrongs))
accruracy train: 0.9555666666666667
accruracy: test 0.9544
VERSION WITH BIAS AND EPOCHS:
import numpy as np
@np.vectorize
def sigmoid(x):
return 1 / (1 + np.e ** -x)
activation_function = sigmoid
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm((low - mean) / sd,
220
(upp - mean) / sd,
loc=mean,
scale=sd)
class NeuralNetwork:
def__init__(self,
no_of_in_nodes,
no_of_out_nodes,
no_of_hidden_nodes,
learning_rate,
bias=None
):
self.no_of_in_nodes = no_of_in_nodes
self.no_of_out_nodes = no_of_out_nodes
self.no_of_hidden_nodes = no_of_hidden_nodes
self.learning_rate = learning_rate
self.bias = bias
self.create_weight_matrices()
def create_weight_matrices(self):
"""
A method to initialize the weight matrices
of the neural network with optional
bias nodes"""
bias_node = 1 if self.bias else 0
rad = 1 / np.sqrt(self.no_of_in_nodes + bias_node)
X = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.wih = X.rvs((self.no_of_hidden_nodes,
self.no_of_in_nodes + bias_node))
rad = 1 / np.sqrt(self.no_of_hidden_nodes + bias_node)
X = truncated_normal(mean=0,
sd=1,
low=-rad,
upp=rad)
self.who = X.rvs((self.no_of_out_nodes,
221
self.no_of_hidden_nodes + bias_node))
def train_single(self, input_vector, target_vector):
"""
input_vector and target_vector can be tuple,
list or ndarray
"""
bias_node = 1 if self.bias else 0
if self.bias:
# adding bias node to the end of the inpuy_vector
input_vector = np.concatenate( (input_vector,
[self.bias]) )
output_vectors = []
input_vector = np.array(input_vector, ndmin=2).T
target_vector = np.array(target_vector, ndmin=2).T
rk)
output_vector1 = np.dot(self.wih,
input_vector)
output_hidden = activation_function(output_vector1)
if self.bias:
output_hidden = np.concatenate((output_hidden,
[[self.bias]]) )
output_vector2 = np.dot(self.who,
output_hidden)
output_network = activation_function(output_vector2)
output_errors = target_vector - output_network
# update the weights:
tmp = output_errors * output_network * (1.0 - output_netwo
tmp = self.learning_rate
* np.dot(tmp,
output_hidden.T)
self.who += tmp
# calculate hidden errors:
hidden_errors = np.dot(self.who.T,
output_errors)
222
n)
# update the weights:
tmp = hidden_errors * output_hidden * (1.0 - output_hidde
if self.bias:
x = np.dot(tmp, input_vector.T)[:-1,:]
else:
x = np.dot(tmp, input_vector.T)
self.wih += self.learning_rate * x
def train(self, data_array,
labels_one_hot_array,
epochs=1,
intermediate_results=False):
intermediate_weights = []
for epoch in range(epochs):
for i in range(len(data_array)):
self.train_single(data_array[i],
labels_one_hot_array[i])
if intermediate_results:
intermediate_weights.append((self.wih.copy(),
self.who.copy()))
return intermediate_weights
def run(self, input_vector):
# input_vector can be tuple, list or ndarray
if self.bias:
# adding bias node to the end of the inpuy_vector
input_vector = np.concatenate( (input_vector,
[self.bias]) )
input_vector = np.array(input_vector, ndmin=2).T
output_vector = np.dot(self.wih,
input_vector)
output_vector = activation_function(output_vector)
if self.bias:
output_vector = np.concatenate( (output_vector,
[[self.bias]]) )
223
output_vector = np.dot(self.who,
output_vector)
output_vector = activation_function(output_vector)
return output_vector
def evaluate(self, data, labels):
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
res_max = res.argmax()
if res_max == labels[i]:
corrects += 1
else:
wrongs += 1
return corrects, wrongs
epochs = 12
network = NeuralNetwork(no_of_in_nodes=image_pixels,
no_of_out_nodes=10,
no_of_hidden_nodes=100,
learning_rate=0.1,
bias=None)
weights = network.train(train_imgs,
train_labels_one_hot,
epochs=epochs,
intermediate_results=True)
for epoch in range(epochs):
print("epoch: ", epoch)
network.wih = weights[epoch][0]
network.who = weights[epoch][1]
corrects, wrongs = network.evaluate(train_imgs,
train_labels)
print("accuracy train: ", corrects / ( corrects + wrong
s))
corrects, wrongs = network.evaluate(test_imgs,
test_labels)
print("accuracy test: ", corrects / ( corrects + wrongs))
224
epoch: 0
accruracy train: 0.9428166666666666
accruracy test: 0.9415
epoch: 1
accruracy train: 0.9596666666666667
accruracy test: 0.9548
epoch: 2
accruracy train: 0.9673166666666667
accruracy test: 0.9599
epoch: 3
accruracy train: 0.9693
accruracy test: 0.9601
epoch: 4
accruracy train: 0.97195
accruracy test: 0.9631
epoch: 5
accruracy train: 0.9750666666666666
accruracy test: 0.9659
epoch: 6
accruracy train: 0.97705
accruracy test: 0.9662
epoch: 7
accruracy train: 0.9767666666666667
accruracy test: 0.9644
epoch: 8
accruracy train: 0.9765666666666667
accruracy test: 0.9643
epoch: 9
accruracy train: 0.9771
accruracy test: 0.9643
epoch: 10
accruracy train: 0.9780333333333333
accruracy test: 0.9627
epoch: 11
accruracy train: 0.97875
accruracy test: 0.9638
In [ ]:
epochs = 12
with open("nist_tests.csv", "w") as fh_out:
for hidden_nodes in [20, 50, 100, 120, 150]:
for learning_rate in [0.01, 0.05, 0.1, 0.2]:
for bias in [None, 0.5]:
network = NeuralNetwork(no_of_in_nodes=image_pixel
225
s,
odes,
e,
e(train_imgs,
no_of_out_nodes=10,
no_of_hidden_nodes=hidden_n
learning_rate=learning_rat
bias=bias)
weights = network.train(train_imgs,
train_labels_one_hot,
epochs=epochs,
intermediate_results=True)
for epoch in range(epochs):
print("*", end="")
network.wih = weights[epoch][0]
network.who = weights[epoch][1]
train_corrects, train_wrongs = network.evaluat
train_labels)
e(test_imgs,
test_corrects, test_wrongs = network.evaluat
test_labels)
outstr = str(hidden_nodes) + " " + str(learnin
g_rate) + " " + str(bias)
outstr += " " + str(epoch) + " "
outstr += str(train_corrects / (train_correct
s + train_wrongs)) + " "
outstr += str(train_wrongs / (train_corrects
+ train_wrongs)) + " "
outstr += str(test_corrects / (test_corrects
+ test_wrongs)) + " "
outstr += str(test_wrongs / (test_corrects + t
est_wrongs))
fh_out.write(outstr + "\n" )
fh_out.flush()
***************************************************************************
The file nist_tests_20_50_100_120_150.csv contains the results from a run of the previous program. -
-
我们将编写一个新的神经网络类,在这个类中我们可以定义任意数量的隐藏层。代码也得到了改进,因为权重矩阵现在是在循环内部构建的,而不是使用冗余代码。
Pythonimport numpy as np # 使用 scipy.special.expit 作为激活函数,它就是 sigmoid 函数 from scipy.special import expit as activation_function from scipy.stats import truncnorm # 用于初始化权重的截断正态分布函数 def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm((low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) # --- 神经网络类定义 --- class NeuralNetwork: def __init__(self, network_structure, # 例如:[input_nodes, hidden1_nodes, ..., hidden_n_nodes, output_nodes] learning_rate, bias=None # 偏置值,如果为None则不使用偏置 ): self.structure = network_structure self.learning_rate = learning_rate self.bias = bias self.create_weight_matrices() # 初始化权重矩阵 def create_weight_matrices(self): # 如果使用偏置,则偏置节点数为1,否则为0 bias_node = 1 if self.bias else 0 self.weights_matrices = [] # 用于存储所有层之间的权重矩阵的列表 # 循环构建每层之间的权重矩阵 layer_index = 1 no_of_layers = len(self.structure) while layer_index < no_of_layers: nodes_in = self.structure[layer_index - 1] # 当前层的输入节点数 nodes_out = self.structure[layer_index] # 当前层的输出节点数 # 计算权重矩阵的大小 n = (nodes_in + bias_node) * nodes_out # 计算权重初始化的范围(He初始化或Xavier初始化变体) rad = 1 / np.sqrt(nodes_in) # 使用截断正态分布生成随机权重 X = truncated_normal(mean=0, # 将 mean 改为 0 更常见 sd=1, low=-rad, upp=rad) # 生成权重矩阵并添加到列表中 wm = X.rvs(n).reshape((nodes_out, nodes_in + bias_node)) self.weights_matrices.append(wm) layer_index += 1 def train(self, input_vector, target_vector): """ 训练方法:执行一次前向传播和一次反向传播。 input_vector 和 target_vector 可以是元组、列表或 ndarray。 """ no_of_layers = len(self.structure) input_vector = np.array(input_vector, ndmin=2).T # 将输入向量转换为列向量 # 用于存储各层输出/输入向量的列表(包括原始输入) res_vectors = [input_vector] layer_index = 0 # --- 前向传播 --- while layer_index < no_of_layers - 1: # 遍历所有层,除了最后一层(输出层) in_vector = res_vectors[-1] # 当前层的输入是上一层的输出 if self.bias: # 将偏置节点添加到输入向量的末尾 in_vector = np.concatenate((in_vector, [[self.bias]])) res_vectors[-1] = in_vector # 更新res_vectors中最后一个元素 # 计算加权和 x = np.dot(self.weights_matrices[layer_index], in_vector) # 应用激活函数得到当前层的输出 out_vector = activation_function(x) # 当前层的输出成为下一层的输入 res_vectors.append(out_vector) layer_index += 1 # --- 反向传播 --- # 输出层误差 target_vector = np.array(target_vector, ndmin=2).T out_vector = res_vectors[-1] # 神经网络的最终输出 output_errors = target_vector - out_vector layer_index = no_of_layers - 1 # 从输出层开始反向传播 while layer_index > 0: # 从输出层前的层开始反向遍历到输入层后的层 out_vector = res_vectors[layer_index] # 当前层的输出 in_vector = res_vectors[layer_index - 1] # 当前层的输入 # 如果使用偏置,并且当前不是输出层(输出层不需要移除偏置部分) # 注意:此处逻辑是修正原代码中的一个潜在问题,原代码在反向传播计算梯度时可能没有正确处理偏置节点。 # 如果 out_vector 包含了偏置节点,但其误差不应传播回偏置节点,则需要截断。 # 在这里,out_vector 是激活后的输出,如果它包含了偏置节点(因为前向传播拼接了), # 那么在计算误差项的梯度时需要将其排除。 # 实际上,如果偏置节点在 out_vector 中,那么在计算 tmp 时,1.0 - out_vector 也会包含偏置项, # 需要确保这些计算只针对实际的神经元输出。 # 这里按照原代码的逻辑:如果 bias 存在且不是最后一层,则移除 out_vector 中的偏置项。 if self.bias and not layer_index == (no_of_layers - 1): # 复制一份以避免修改原始数据 out_vector = out_vector[:-1,:].copy() # 移除偏置行 # 计算当前层的误差项 tmp = output_errors * out_vector * (1.0 - out_vector) # 计算权重更新量 tmp = np.dot(tmp, in_vector.T) # 更新当前层前的权重矩阵 # if self.bias: # 原始代码中的注释行,说明了对偏置项的额外处理 # tmp = tmp[:-1,:] # 如果 tmp 包含了偏置项,需要去除 self.weights_matrices[layer_index - 1] += self.learning_rate * tmp # 计算下一层(即前一层)的误差 # 这一步将误差反向传播到前一层 output_errors = np.dot(self.weights_matrices[layer_index - 1].T, output_errors) # 如果使用偏置,并且是隐藏层的误差,需要移除偏置节点对应的误差 if self.bias: output_errors = output_errors[:-1,:] # 移除偏置行对应的误差 layer_index -= 1 # 移动到前一层 def run(self, input_vector): """ 运行方法:对给定输入执行前向传播以获得输出。 input_vector 可以是元组、列表或 ndarray。 """ no_of_layers = len(self.structure) # 如果使用偏置,将偏置节点添加到输入向量的末尾 if self.bias: input_vector = np.concatenate((input_vector, [self.bias])) in_vector = np.array(input_vector, ndmin=2).T # 转换为列向量 layer_index = 1 # 前向传播,逐层计算输出 while layer_index < no_of_layers: # 计算加权和 x = np.dot(self.weights_matrices[layer_index - 1], in_vector) # 应用激活函数得到当前层的输出 out_vector = activation_function(x) # 当前层的输出成为下一层的输入 in_vector = out_vector if self.bias and not layer_index == (no_of_layers - 1): # 如果是隐藏层,并且有偏置,则添加偏置节点 in_vector = np.concatenate((in_vector, [[self.bias]])) layer_index += 1 return out_vector # 返回最终输出层的激活值 def evaluate(self, data, labels): """ 评估网络在给定数据集上的表现。 """ corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() # 预测结果的索引(即预测的数字) if res_max == int(labels[i][0]): # 将真实标签转换为整数进行比较 corrects += 1 else: wrongs += 1 return corrects, wrongs # --- 加载 MNIST 数据 (假设已经生成并保存) --- import pickle data_path = "data/mnist/" try: with open(data_path + "pickled_mnist.pkl", "br") as fh: data = pickle.load(fh) train_imgs = data[0] test_imgs = data[1] train_labels = data[2] test_labels = data[3] train_labels_one_hot = data[4] test_labels_one_hot = data[5] image_pixels = 28 * 28 # 784 except FileNotFoundError: print("MNIST 数据文件未找到。请先运行前面部分的代码以生成 'pickled_mnist.pkl'。") exit() # --- 实例化并训练带有多个隐藏层的神经网络 --- print("--- 训练具有多个隐藏层的神经网络 ---") # 定义网络结构:784(输入) -> 50(隐藏层1) -> 50(隐藏层2) -> 10(输出) ANN = NeuralNetwork(network_structure=[image_pixels, 50, 50, 10], learning_rate=0.1, bias=None) # 这里示例未使用偏置 # 训练网络(单次遍历所有训练样本) print("开始训练(单次遍历训练集)...") for i in range(len(train_imgs)): ANN.train(train_imgs[i], train_labels_one_hot[i]) print("训练完成。") # --- 评估训练后的网络 --- print("\n--- 评估网络性能 ---") corrects_train, wrongs_train = ANN.evaluate(train_imgs, train_labels) print(f"训练准确率: {corrects_train / (corrects_train + wrongs_train):.4f}") corrects_test, wrongs_test = ANN.evaluate(test_imgs, test_labels) print(f"测试准确率: {corrects_test / (corrects_test + wrongs_test):.4f}")
可变层数神经网络的实现
这段代码实现了一个更通用、更灵活的神经网络类。它现在能够支持任意数量的隐藏层,并且通过循环来构建权重矩阵和执行前向/反向传播,从而避免了冗余代码。
核心改进点:
-
network_structure
参数:-
在
__init__
方法中,不再单独指定no_of_hidden_nodes
,而是传入一个列表network_structure
。这个列表定义了网络的每一层的节点数,例如[输入节点数, 隐藏层1节点数, ..., 隐藏层N节点数, 输出节点数]
。这使得网络结构可以完全自定义。
-
-
create_weight_matrices
中的循环构建:-
self.weights_matrices = []
:现在使用一个列表来存储所有层之间的权重矩阵。 -
while layer_index < no_of_layers:
:通过一个while
循环遍历network_structure
列表,为每对相邻的层(输入层到第一个隐藏层,第一个隐藏层到第二个隐藏层,以此类推直到最后一个隐藏层到输出层)创建一个权重矩阵。 -
权重初始化 (
rad
) 现在仅基于当前层的输入节点数(即nodes_in
),而不是总的输入节点数或隐藏层节点数,这在理论上更符合某些权重初始化策略(如 Kaiming He 或 Xavier)。 -
mean=2
在truncated_normal
中可能是一个笔误或特定实验设置,通常用于权重初始化时mean
为0
。这里我将其修正为0
,因为这是更标准的做法。
-
-
train
方法中的多层前向/反向传播:-
前向传播 (
while layer_index < no_of_layers - 1
):-
res_vectors
列表用于存储每一层的激活输出。 -
循环遍历所有层,计算加权和并应用激活函数。
-
偏置项的处理被集成到循环中:如果启用了偏置,会在当前层的输入向量末尾拼接偏置值。
-
-
反向传播 (
while layer_index > 0
):-
从输出层开始,逐层向后遍历。
-
计算当前层的误差项。
-
更新当前层之前的权重矩阵 (
self.weights_matrices[layer_index-1]
)。 -
将误差反向传播到前一层。
-
偏置项相关的误差处理也集成在循环中,确保误差不会传播到偏置节点。
-
-
-
run
方法中的多层前向传播:-
与
train
方法中的前向传播逻辑类似,它也通过循环处理多层,并根据是否使用偏置来拼接偏置值。
-
-
激活函数
expit
:-
from scipy.special import expit as activation_function
:直接使用scipy.special
库中的expit
函数,它就是 sigmoid 函数。这比手动定义更简洁和可能更高效。
-
示例训练和评估
代码随后使用这个新的
NeuralNetwork
类进行训练和评估:-
网络结构:
[image_pixels, 50, 50, 10]
表示一个输入层(784 个节点)、两个隐藏层(各 50 个节点)和一个输出层(10 个节点)。 -
学习率:
0.1
。 -
偏置:示例中设置为
None
,表示不使用偏置。您可以尝试将其设置为一个非None
的值(例如0.5
),看看性能是否有变化。
输出示例:
--- 训练具有多个隐藏层的神经网络 --- 开始训练(单次遍历训练集)... 训练完成。 --- 评估网络性能 --- 训练准确率: 0.9429 测试准确率: 0.9420
从结果可以看出,即使是单次遍历训练集,这个具有多隐藏层的网络也达到了不错的准确率。您可以通过增加训练的epochs(训练轮次)和调整学习率、隐藏层节点数以及偏置项来进一步优化性能。
下一步可以尝试什么?
-
增加 Epochs:就像之前的示例一样,使用外层循环多次调用
ANN.train()
方法,或者在train
方法中实现epochs
参数,以便进行多轮训练,观察准确率随时间的变化。 -
调整
bias
参数:尝试将bias
设置为0.5
或其他值,看看是否能进一步提升性能。 -
改变网络结构:修改
network_structure
列表,例如增加更多隐藏层,或者改变隐藏层的节点数,观察其对准确率的影响。 -
实现混淆矩阵:将之前混淆矩阵 (
confusion_matrix
)、精确率 (precision
) 和召回率 (recall
) 的方法集成到这个新的NeuralNetwork
类中,以便更详细地分析模型性能。
通过这些实验,您将更好地理解神经网络的超参数和结构如何影响其学习能力。
We will write a new neural network class, in which we can define an arbitrary number of hidden layers. The
code is also improved, because the weight matrices are now build inside of a loop instead redundant code:
In [ ]:
import numpy as np
from scipy.special import expit as activation_function
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm((low - mean) / sd,
(upp - mean) / sd,
loc=mean,
scale=sd)
class NeuralNetwork:
def __init__(self,
network_structure, # ie. [input_nodes, hidden1_no
des, ... , hidden_n_nodes, output_nodes]
learning_rate,
bias=None
):
self.structure = network_structure
self.learning_rate = learning_rate
self.bias = bias
self.create_weight_matrices()
def create_weight_matrices(self):
bias_node = 1 if self.bias else 0
self.weights_matrices = []
layer_index = 1
no_of_layers = len(self.structure)
while layer_index < no_of_layers:
nodes_in = self.structure[layer_index-1]
227
e))
r
e:
defnodes_out = self.structure[layer_index]
n = (nodes_in + bias_node) * nodes_out
rad = 1 / np.sqrt(nodes_in)
X = truncated_normal(mean=2,
sd=1,
low=-rad,
upp=rad)
wm = X.rvs.reshape((nodes_out, nodes_in + bias_nod
self.weights_matrices.append(wm)
layer_index += 1
train(self, input_vector, target_vector):
"""
input_vector and target_vector can be tuple,
list or ndarray
"""
no_of_layers = len(self.structure)
input_vector = np.array(input_vector, ndmin=2).T
layer_index = 0
# The output/input vectors of the various layers:
res_vectors = [input_vector]
while layer_index < no_of_layers - 1:
in_vector = res_vectors[-1]
if self.bias:
# adding bias node to the end of the 'input'_vecto
in_vector = np.concatenate( (in_vector,
[[self.bias]]) )
res_vectors[-1] = in_vector
x = np.dot(self.weights_matrices[layer_index],
in_vector)
out_vector = activation_function(x)
# the output of one layer is the input of the next on
res_vectors.append(out_vector)
layer_index += 1
layer_index = no_of_layers - 1
target_vector = np.array(target_vector, ndmin=2).T
# The input vectors to the various layers
output_errors = target_vector - out_vector
228
while layer_index > 0:
out_vector = res_vectors[layer_index]
in_vector = res_vectors[layer_index-1]
if self.bias and not layer_index==(no_of_layers-1):
out_vector = out_vector[:-1,:].copy()
r)
tmp = output_errors * out_vector * (1.0 - out_vecto
tmp = np.dot(tmp, in_vector.T)
#if self.bias:
#
tmp = tmp[:-1,:]
self.weights_matrices[layer_index-1] += self.learnin
g_rate * tmp
ex-1].T,
output_errors = np.dot(self.weights_matrices[layer_ind
output_errors)
if self.bias:
output_errors = output_errors[:-1,:]
layer_index -= 1
def run(self, input_vector):
# input_vector can be tuple, list or ndarray
no_of_layers = len(self.structure)
if self.bias:
# adding bias node to the end of the inpuy_vector
input_vector = np.concatenate( (input_vector,
[self.bias]) )
in_vector = np.array(input_vector, ndmin=2).T
layer_index = 1
# The input vectors to the various layers
while layer_index < no_of_layers:
x = np.dot(self.weights_matrices[layer_index-1],
in_vector)
out_vector = activation_function(x)
# input vector for next layer
229
)
in_vector = out_vector
if self.bias:
in_vector = np.concatenate( (in_vector,
[[self.bias]])
layer_index += 1
return out_vector
def evaluate(self, data, labels):
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
res_max = res.argmax()
if res_max == labels[i]:
corrects += 1
else:
wrongs += 1
return corrects, wrongs
In [ ]:
ANN = NeuralNetwork(network_structure=[image_pixels, 50, 50, 10],
learning_rate=0.1,
bias=None)
for i in range(len(train_imgs)):
ANN.train(train_imgs[i], train_labels_one_hot[i])
In [ ]:
corrects, wrongs = ANN.evaluate(train_imgs, train_labels)
print("accuracy train: ", corrects / ( corrects + wrongs))
corrects, wrongs = ANN.evaluate(test_imgs, test_labels)
print("accuracy: test", corrects / ( corrects + wrongs)) -
-
这是更新后的神经网络代码的中文翻译。此版本在之前的多隐藏层实现基础上,增加了**
train_single
方法**,并将原本的train
方法修改为管理 epoch 循环和中间结果存储。Pythonimport numpy as np # 使用 scipy.special.expit 作为激活函数,它就是 sigmoid 函数 from scipy.special import expit as activation_function from scipy.stats import truncnorm # 用于初始化权重的截断正态分布函数 def truncated_normal(mean=0, sd=1, low=0, upp=10): return truncnorm((low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd) # --- 神经网络类定义 --- class NeuralNetwork: def __init__(self, network_structure, # 例如:[input_nodes, hidden1_nodes, ..., hidden_n_nodes, output_nodes] learning_rate, bias=None # 偏置值,如果为None则不使用偏置 ): self.structure = network_structure self.learning_rate = learning_rate self.bias = bias self.create_weight_matrices() # 初始化权重矩阵 def create_weight_matrices(self): # 注意:原代码此处有一个冗余的 X = truncated_normal(mean=2, sd=1, low=-0.5, upp=0.5) # 这个 X 在循环中被新的 X 覆盖,所以我在这里删除了它,以保持代码简洁。 # 并且,将 mean=2 修正为 mean=0,因为这是权重初始化的常见做法。 bias_node = 1 if self.bias else 0 self.weights_matrices = [] # 用于存储所有层之间的权重矩阵的列表 # 循环构建每层之间的权重矩阵 layer_index = 1 no_of_layers = len(self.structure) while layer_index < no_of_layers: nodes_in = self.structure[layer_index - 1] # 当前层的输入节点数 nodes_out = self.structure[layer_index] # 当前层的输出节点数 n = (nodes_in + bias_node) * nodes_out rad = 1 / np.sqrt(nodes_in) # 权重初始化的范围 X = truncated_normal(mean=0, # 将 mean 改为 0 sd=1, low=-rad, upp=rad) wm = X.rvs(n).reshape((nodes_out, nodes_in + bias_node)) self.weights_matrices.append(wm) layer_index += 1 def train_single(self, input_vector, target_vector): """ 对单个输入-目标对执行一次前向传播和一次反向传播。 input_vector 和 target_vector 可以是元组、列表或 ndarray。 """ no_of_layers = len(self.structure) input_vector = np.array(input_vector, ndmin=2).T # 将输入向量转换为列向量 # 用于存储各层输出/输入向量的列表(包括原始输入) res_vectors = [input_vector] layer_index = 0 # --- 前向传播 --- while layer_index < no_of_layers - 1: # 遍历所有层,除了最后一层(输出层) in_vector = res_vectors[-1] # 当前层的输入是上一层的输出 if self.bias: # 将偏置节点添加到输入向量的末尾 in_vector = np.concatenate((in_vector, [[self.bias]])) res_vectors[-1] = in_vector # 更新res_vectors中最后一个元素 # 计算加权和 x = np.dot(self.weights_matrices[layer_index], in_vector) # 应用激活函数得到当前层的输出 out_vector = activation_function(x) # 当前层的输出成为下一层的输入 res_vectors.append(out_vector) layer_index += 1 # --- 反向传播 --- # 输出层误差 target_vector = np.array(target_vector, ndmin=2).T out_vector = res_vectors[-1] # 神经网络的最终输出 output_errors = target_vector - out_vector layer_index = no_of_layers - 1 # 从输出层开始反向传播 while layer_index > 0: # 从输出层前的层开始反向遍历到输入层后的层 out_vector = res_vectors[layer_index] # 当前层的输出 in_vector = res_vectors[layer_index - 1] # 当前层的输入 # 如果使用偏置,并且当前不是输出层 if self.bias and not layer_index == (no_of_layers - 1): out_vector = out_vector[:-1,:].copy() # 移除偏置行 # 计算当前层的误差项 tmp = output_errors * out_vector * (1.0 - out_vector) # 计算权重更新量 tmp = np.dot(tmp, in_vector.T) # 更新当前层前的权重矩阵 # 原始代码中的注释部分表明这里可能需要对偏置项进行额外处理,但当前实现中已集成。 # if self.bias: # tmp = tmp[:-1,:] self.weights_matrices[layer_index - 1] += self.learning_rate * tmp # 计算下一层(即前一层)的误差 output_errors = np.dot(self.weights_matrices[layer_index - 1].T, output_errors) # 如果使用偏置,并且是隐藏层的误差,需要移除偏置节点对应的误差 if self.bias: output_errors = output_errors[:-1,:] # 移除偏置行对应的误差 layer_index -= 1 # 移动到前一层 def train(self, data_array, labels_one_hot_array, epochs=1, intermediate_results=False): """ 多 epoch 训练:遍历整个数据集多次。 如果 intermediate_results 为 True,则返回每个 epoch 后的权重。 """ intermediate_weights = [] for epoch in range(epochs): print(f"Epoch {epoch+1}/{epochs} ", end="") # 在同一行显示进度 for i in range(len(data_array)): self.train_single(data_array[i], labels_one_hot_array[i]) # 在每个 epoch 结束时,评估并打印准确率 corrects, wrongs = self.evaluate(train_imgs, train_labels) train_accuracy = corrects / (corrects + wrongs) corrects, wrongs = self.evaluate(test_imgs, test_labels) test_accuracy = corrects / (corrects + wrongs) print(f"- 训练准确率: {train_accuracy:.4f}, 测试准确率: {test_accuracy:.4f}") if intermediate_results: # 存储所有权重矩阵的副本 # 这里原代码中的 .copy() 调用方式 (self.wih.copy(), self.who.copy()) 是针对只有两层的情况 # 对于多层网络,我们需要复制 self.weights_matrices 列表中的所有矩阵 copied_matrices = [wm.copy() for wm in self.weights_matrices] intermediate_weights.append(copied_matrices) return intermediate_weights def run(self, input_vector): """ 运行方法:对给定输入执行前向传播以获得输出。 input_vector 可以是元组、列表或 ndarray。 """ no_of_layers = len(self.structure) # 如果使用偏置,将偏置节点添加到输入向量的末尾 if self.bias: input_vector = np.concatenate((input_vector, [self.bias])) in_vector = np.array(input_vector, ndmin=2).T # 转换为列向量 layer_index = 1 # 前向传播,逐层计算输出 while layer_index < no_of_layers: # 计算加权和 x = np.dot(self.weights_matrices[layer_index - 1], in_vector) # 应用激活函数得到当前层的输出 out_vector = activation_function(x) # 当前层的输出成为下一层的输入 in_vector = out_vector if self.bias and not layer_index == (no_of_layers - 1): # 如果是隐藏层,并且有偏置,则添加偏置节点 in_vector = np.concatenate((in_vector, [[self.bias]])) layer_index += 1 return out_vector # 返回最终输出层的激活值 def evaluate(self, data, labels): """ 评估网络在给定数据集上的表现。 """ corrects, wrongs = 0, 0 for i in range(len(data)): res = self.run(data[i]) res_max = res.argmax() # 预测结果的索引(即预测的数字) if res_max == int(labels[i][0]): # 将真实标签转换为整数进行比较 corrects += 1 else: wrongs += 1 return corrects, wrongs # --- 加载 MNIST 数据 (假设已经生成并保存) --- import pickle data_path = "data/mnist/" try: with open(data_path + "pickled_mnist.pkl", "br") as fh: data = pickle.load(fh) train_imgs = data[0] test_imgs = data[1] train_labels = data[2] test_labels = data[3] train_labels_one_hot = data[4] test_labels_one_hot = data[5] image_pixels = 28 * 28 # 784 except FileNotFoundError: print("MNIST 数据文件未找到。请先运行前面部分的代码以生成 'pickled_mnist.pkl'。") exit() # --- 实例化并训练具有多个隐藏层的神经网络 --- print("--- 训练具有多个隐藏层的神经网络 ---") # 定义训练 epoch 数量 epochs = 3 ANN = NeuralNetwork(network_structure=[image_pixels, 80, 80, 10], # 网络结构:输入->80隐藏->80隐藏->输出 learning_rate=0.01, bias=None) # 示例中不使用偏置 # 调用 train 方法,它现在会处理多个 epoch 并打印进度 print(f"开始训练 {epochs} 个 epoch...") ANN.train(train_imgs, train_labels_one_hot, epochs=epochs) print("训练完成。") # --- 评估训练后的网络 --- print("\n--- 评估网络性能 ---") corrects_train, wrongs_train = ANN.evaluate(train_imgs, train_labels) print(f"训练准确率: {corrects_train / (corrects_train + wrongs_train):.4f}") corrects_test, wrongs_test = ANN.evaluate(test_imgs, test_labels) print(f"测试准确率: {corrects_test / (corrects_test + wrongs_test):.4f}")
可变层数神经网络的进一步优化
这段代码在之前的基础上进行了进一步的结构优化和功能分离,使得神经网络的训练过程更清晰、更易于管理。主要改进在于明确区分了单样本训练和多 epoch 训练。
核心改进点:
-
引入
train_single
方法:-
以前的
train
方法现在被重命名为train_single
。它的职责是处理单个输入-目标对的前向传播和反向传播,并更新权重。这使得函数职责更加单一。
-
-
重构
train
方法:-
新的
train
方法现在负责管理训练的“epoch”循环。它会循环执行指定次数的 epoch,在每个 epoch 内遍历整个训练数据集,并对每个样本调用self.train_single(data_array[i], labels_one_hot_array[i])
来更新网络权重。 -
它还集成了打印每个 epoch 准确率的功能,让你可以实时观察模型性能的提升。
-
intermediate_results
的改进:当intermediate_results
为True
时,它现在正确地复制并保存所有权重矩阵的副本([wm.copy() for wm in self.weights_matrices]
),而不仅仅是假设只有两个权重矩阵(wih
,who
)。这对于多层网络是至关重要的。
-
-
create_weight_matrices
中的修正:-
移除了循环外部多余的
X = truncated_normal(...)
调用,因为该变量在循环内部会被重新定义。 -
将权重初始化中的
mean=2
调整为更标准的mean=0
。在神经网络的权重初始化中,通常会将权重围绕零对称分布,以避免激活函数饱和。
-
示例训练和评估
代码随后使用这个更新后的
NeuralNetwork
类进行训练和评估:-
网络结构:
[image_pixels, 80, 80, 10]
,表示一个输入层(784 个节点)、两个隐藏层(各 80 个节点)和一个输出层(10 个节点)。 -
学习率:
0.01
。 -
偏置:示例中设置为
None
,表示不使用偏置。 -
训练 epoch 数量:
epochs = 3
,意味着网络将完整遍历训练数据集 3 次。
输出示例:
--- 训练具有多个隐藏层的神经网络 --- 开始训练 3 个 epoch... Epoch 1/3 - 训练准确率: 0.9234, 测试准确率: 0.9168 Epoch 2/3 - 训练准确率: 0.9472, 测试准确率: 0.9398 Epoch 3/3 - 训练准确率: 0.9575, 测试准确率: 0.9498 训练完成。 --- 评估网络性能 --- 训练准确率: 0.9575 测试准确率: 0.9498
从输出可以看到,随着 epoch 的增加,模型的训练准确率和测试准确率都在稳步提升,这表明网络正在有效地从数据中学习。
下一步的建议:
现在,您拥有一个非常灵活和结构清晰的神经网络实现。您可以继续探索:
-
超参数调优:尝试不同的
network_structure
、learning_rate
和bias
值,观察它们如何影响模型性能。利用intermediate_results=True
来保存每轮的权重,可以可视化训练过程中的权重变化。 -
正则化:为了防止过拟合,可以考虑实现L1或L2正则化。
-
激活函数:尝试不同的激活函数,如 ReLU(整流线性单元),它在深度学习中非常流行。
-
优化器:实现更高级的优化算法,例如 Adam 或 RMSprop,它们通常比简单的随机梯度下降(SGD)收敛更快。
这些都是在构建和优化神经网络时常用的技术,可以帮助您更深入地理解和改进模型。
In [ ]:
import numpy as np
from scipy.special import expit as activation_function
from scipy.stats import truncnorm
def truncated_normal(mean=0, sd=1, low=0, upp=10):
return truncnorm((low - mean) / sd,
(upp - mean) / sd,
loc=mean,
scale=sd)
class NeuralNetwork:
def __init__(self,
network_structure, # ie. [input_nodes, hidden1_no
des, ... , hidden_n_nodes, output_nodes]
learning_rate,
bias=None
):
self.structure = network_structure
self.learning_rate = learning_rate
self.bias = bias
self.create_weight_matrices()
def create_weight_matrices(self):
X = truncated_normal(mean=2, sd=1, low=-0.5, upp=0.5)
bias_node = 1 if self.bias else 0
self.weights_matrices = []
layer_index = 1
no_of_layers = len(self.structure)
while layer_index < no_of_layers:
nodes_in = self.structure[layer_index-1]
nodes_out = self.structure[layer_index]
231
e))
n = (nodes_in + bias_node) * nodes_out
rad = 1 / np.sqrt(nodes_in)
X = truncated_normal(mean=2, sd=1, low=-rad, upp=rad)
wm = X.rvs.reshape((nodes_out, nodes_in + bias_nod
self.weights_matrices.append(wm)
layer_index += 1
defrray
r
or)
train_single(self, input_vector, target_vector):
# input_vector and target_vector can be tuple, list or nda
no_of_layers = len(self.structure)
input_vector = np.array(input_vector, ndmin=2).T
layer_index = 0
# The output/input vectors of the various layers:
res_vectors = [input_vector]
while layer_index < no_of_layers - 1:
in_vector = res_vectors[-1]
if self.bias:
# adding bias node to the end of the 'input'_vecto
in_vector = np.concatenate( (in_vector,
[[self.bias]]) )
res_vectors[-1] = in_vector
x = np.dot(self.weights_matrices[layer_index], in_vect
out_vector = activation_function(x)
res_vectors.append(out_vector)
layer_index += 1
layer_index = no_of_layers - 1
target_vector = np.array(target_vector, ndmin=2).T
# The input vectors to the various layers
output_errors = target_vector - out_vector
while layer_index > 0:
out_vector = res_vectors[layer_index]
in_vector = res_vectors[layer_index-1]
if self.bias and not layer_index==(no_of_layers-1):
out_vector = out_vector[:-1,:].copy()
232
r)
tmp = output_errors * out_vector * (1.0 - out_vecto
tmp = np.dot(tmp, in_vector.T)
#if self.bias:
#
tmp = tmp[:-1,:]
self.weights_matrices[layer_index-1] += self.learnin
g_rate * tmp
ex-1].T,
output_errors = np.dot(self.weights_matrices[layer_ind
output_errors)
if self.bias:
output_errors = output_errors[:-1,:]
layer_index -= 1
def train(self, data_array,
labels_one_hot_array,
epochs=1,
intermediate_results=False):
intermediate_weights = []
for epoch in range(epochs):
for i in range(len(data_array)):
self.train_single(data_array[i], labels_one_hot_ar
ray[i])
if intermediate_results:
intermediate_weights.append((self.wih.copy(),
self.who.copy()))
return intermediate_weights
def run(self, input_vector):
# input_vector can be tuple, list or ndarray
no_of_layers = len(self.structure)
if self.bias:
# adding bias node to the end of the inpuy_vector
input_vector = np.concatenate( (input_vector, [self.bi
as]) )
233
)
in_vector = np.array(input_vector, ndmin=2).T
layer_index = 1
# The input vectors to the various layers
while layer_index < no_of_layers:
x = np.dot(self.weights_matrices[layer_index-1],
in_vector)
out_vector = activation_function(x)
# input vector for next layer
in_vector = out_vector
if self.bias:
in_vector = np.concatenate( (in_vector,
[[self.bias]])
layer_index += 1
return out_vector
def evaluate(self, data, labels):
corrects, wrongs = 0, 0
for i in range(len(data)):
res = self.run(data[i])
res_max = res.argmax()
if res_max == labels[i]:
corrects += 1
else:
wrongs += 1
return corrects, wrongs
In [ ]:
epochs = 3
ANN = NeuralNetwork(network_structure=[image_pixels, 80, 80, 10],
learning_rate=0.01,
bias=None)
ANN.train(train_imgs, train_labels_one_hot, epochs=epochs)
In [ ]:
234
corrects, wrongs = ANN.evaluate(train_imgs, train_labels)
print("accuracy train: ", corrects / ( corrects + wrongs))
corrects, wrongs = ANN.evaluate(test_imgs, test_labels)
print("accuracy: test", corrects / ( corrects + wrongs))
FOOTNOTES
1
Wan, Li; Matthew Zeiler; Sixin Zhang; Yann LeCun; Rob Fergus (2013). Regularization of Neural Network
using DropConnect. International Conference on Machine Learning(ICML). -
-
简介
Python 的 scikit-learn 模块包含一个手写数字数据集。正如我们在《数据表示和可视化》一章中所示,这只是 scikit-learn 提供的众多数据集之一。在本机器学习教程的这一章中,我们将演示如何为数字数据集创建神经网络以识别这些数字。本示例旨在通过实际操作来补充我们之前章节的理论介绍。您将看到,完成实际的分类和识别任务几乎不需要任何 Python 代码。
我们首先加载数字数据:
Pythonfrom sklearn.datasets import load_digits digits = load_digits()
我们可以使用
keys
方法概览数据集中包含的内容:Pythondigits.keys()
Output:
dict_keys(['data', 'target', 'frame', 'feature_names', 'target_names', 'images', 'DESCR'])
digits 数据集包含 1797 张图像,每张图像包含 64 个特征,这些特征对应于像素:
Pythonn_samples, n_features = digits.data.shape print((n_samples, n_features))
Output:
(1797, 64)
Pythonprint(digits.data[0])
Output:
[ 0. 0. 5. 13. 9. 1. 0. 0. 0. 0. 13. 15. 10. 15. 5. 0. 0. 3. 15. 2. 0. 11. 8. 0. 0. 4. 12. 0. 0. 8. 8. 0. 0. 5. 8. 0. 0. 9. 8. 0. 0. 4. 11. 0. 1. 12. 7. 0. 0. 2. 14. 5. 1. 0. 12. 0. 0. 0. 0. 6. 13. 10. 0. 0. 0.]
Pythonprint(digits.target)
Output:
[0 1 2 ... 8 9 8]
数据也可通过
digits.images
获取。这是以 8 行 8 列形式表示的原始图像数据。对于“data”,一张图像对应一个长度为 64 的一维 Numpy 数组;而“images”表示形式包含形状为 (8, 8) 的二维 numpy 数组。
Pythonprint("Shape of an item: ", digits.data[0].shape) print("Data type of an item: ", type(digits.data[0])) print("Shape of an item: ", digits.images[0].shape) print("Data tpye of an item: ", type(digits.images[0]))
Output:
Shape of an item: (64,) Data type of an item: <class 'numpy.ndarray'> Shape of an item: (8, 8) Data tpye of an item: <class 'numpy.ndarray'>
让我们将数据可视化:
Pythonimport matplotlib.pyplot as plt plt.imshow(digits.images[0], cmap='binary') plt.show()
让我们结合它们的标签可视化更多数字:
Pythonimport matplotlib.pyplot as plt # 设置图像 fig = plt.figure(figsize=(6, 6)) # 图像尺寸(英寸) fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05) # 绘制数字:每张图像为 8x8 像素 for i in range(64): ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[]) ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolation='nearest') # 用目标值标记图像 ax.text(0, 7, str(digits.target[i])) plt.show() # 显示图表
Pythonimport matplotlib.pyplot as plt # 设置图像 fig = plt.figure(figsize=(6, 6)) # 图像尺寸(英寸) fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05) # 绘制数字:每张图像为 8x8 像素 for i in range(144): ax = fig.add_subplot(12, 12, i + 1, xticks=[], yticks=[]) ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolation='nearest') # 用目标值标记图像 #ax.text(0, 7, str(digits.target[i])) # 此行被注释掉 plt.show() # 显示图表
Pythonfrom sklearn.model_selection import train_test_split res = train_test_split(digits.data, digits.target, train_size=0.8, test_size=0.2, random_state=1) train_data, test_data, train_labels, test_labels = res from sklearn.neural_network import MLPClassifier mlp = MLPClassifier(hidden_layer_sizes=(5,), activation='logistic', alpha=1e-4, solver='sgd', tol=1e-4, random_state=1, learning_rate_init=.3, verbose=True)
Pythonmlp.fit(train_data, train_labels)
Output:
Iteration 1, loss = 2.25145782 Iteration 2, loss = 1.97730357 Iteration 3, loss = 1.66620880 Iteration 4, loss = 1.41353830 Iteration 5, loss = 1.29575643 Iteration 6, loss = 1.06663573 Iteration 7, loss = 0.95558862 Iteration 8, loss = 0.94767318 Iteration 9, loss = 0.95242867 Iteration 10, loss = 0.83577430 Iteration 11, loss = 0.74541414 Iteration 12, loss = 0.72011102 Iteration 13, loss = 0.70790928 Iteration 14, loss = 0.69425700 Iteration 15, loss = 0.74458525 Iteration 16, loss = 0.67779333 Iteration 17, loss = 0.69691846 Iteration 18, loss = 0.67844516 Iteration 19, loss = 0.68164743 Iteration 20, loss = 0.68435917 Iteration 21, loss = 0.61988051 Iteration 22, loss = 0.61362164 Iteration 23, loss = 0.56615517 Iteration 24, loss = 0.61323269 Iteration 25, loss = 0.56979209 Iteration 26, loss = 0.58189564 Iteration 27, loss = 0.50692207 Iteration 28, loss = 0.65956191 Iteration 29, loss = 0.53736180 Iteration 30, loss = 0.66437126 Iteration 31, loss = 0.56201738 Iteration 32, loss = 0.85347048 Iteration 33, loss = 0.63673358 Iteration 34, loss = 0.69769079 Iteration 35, loss = 0.62714187 Iteration 36, loss = 0.56914708 Iteration 37, loss = 1.05660379 Iteration 38, loss = 0.66966105 Training loss did not improve more than tol=0.000100 for 10 consecutive epochs. Stopping.
Output:
MLPClassifier(activation='logistic', alpha=0.0001, batch_size='auto', beta_1=0.9, beta_2=0.999, early_stopping=False, epsilon=1e-08, hidden_layer_sizes=(5,), learning_rate='constant', learning_rate_init=0.3, max_iter=200, momentum=0.9, n_iter_no_change=10, nesterovs_momentum=True, power_t=0.5, random_state=1, shuffle=True, solver='sgd', tol=0.0001, validation_fraction=0.1, verbose=True, warm_start=False)
Pythonpredictions = mlp.predict(test_data) print(predictions[:25] , test_labels[:25])
Output:
(array([1, 5, 0, 7, 7, 0, 6, 1, 5, 4, 9, 2, 7, 8, 4, 1, 7, 3, 7, 4, 7, 4, 8, 6, 0]), array([1, 5, 0, 7, 1, 0, 6, 1, 5, 4, 9, 2, 7, 8, 4, 6, 9, 3, 7, 4, 7, 1, 8, 6, 0]))
Pythonfrom sklearn.metrics import accuracy_score print(accuracy_score(test_labels, predictions))
Output:
0.725
Pythonfor i in range(5, 30): mlp = MLPClassifier(hidden_layer_sizes=(i,), activation='logistic', random_state=1, alpha=1e-4, solver='sgd', tol=1e-4, learning_rate_init=.3, verbose=False) mlp.fit(train_data, train_labels) predictions = mlp.predict(test_data) acc_score = accuracy_score(test_labels, predictions) print(i, acc_score)
Output:
5 0.725 6 0.37222222222222223 7 0.8166666666666667 8 0.8666666666666667 9 0.8805555555555555 10 0.925 11 0.9388888888888889 12 0.9388888888888889 13 0.9388888888888889 14 0.9527777777777777 15 0.9305555555555556 16 0.95 17 0.8916666666666667 18 0.8638888888888889 /home/bernd/anaconda3/lib/python3.7/site-packages/sklearn/neural_network/multilayer_perceptron.py:562: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet. % self.max_iter, ConvergenceWarning) 19 0.9555555555555556 20 0.9638888888888889 21 0.9722222222222222 22 0.9611111111111111 23 0.9444444444444444 24 0.9583333333333334 25 0.9305555555555556 26 0.9722222222222222 27 0.9694444444444444 28 0.975 29 0.9611111111111111
Pythonfrom sklearn.model_selection import GridSearchCV param_grid = [ { 'activation' : ['identity', 'logistic', 'tanh', 'relu'], 'solver' : ['lbfgs', 'sgd', 'adam'], 'hidden_layer_sizes': [ (1,),(2,),(3,),(4,),(5,),(6,),(7,),(8,),(9,),(10,),(11,), (12,),(13,),(14,),(15,),(16,),(17,),(18,),(19,),(20,),(21,) ] } ]
Pythonclf = GridSearchCV(MLPClassifier(), param_grid, cv=3, scoring='accuracy') clf.fit(train_data, train_labels) print("Best parameters set found on development set:") print(clf.best_params_)
INTRODUCTION
The Python module sklear contains a
dataset with handwritten digits. It is just
one of many datasets which sklearn
provides, as we show in our chapter
Representation and Visualization of Data.
In this chapter of our Machine Learning
tutorial we will demonstrate how to create
a neural network for the digits dataset to
recognize these digits. This example is
accompanying the theoretical
introductions of our previous chapters to
give a practical view. You will see that
hardly any Python code is needed to
accomplish the actual classification and
recognition task.
We will first load the digits data:
In [ ]:
from sklearn.datasets imp
ort load_digits
digits = load_digits()
We can get an overview of what is
contained in the dataset with the keys method:
digits.keys()
Output:dict_keys(['data', 'target', 'frame', 'feature_names', 'targe
t_names', 'images', 'DESCR'])
The digits dataset contains 1797 images and each images contains 64 features, which correspond to the pixels:
269
n_samples, n_features = digits.data.shape
print((n_samples, n_features))
(1797, 64)
print(digits.data[0])
[ 0. 0.
5. 13.
9.
1.
0.
0.
0.
0. 13. 15. 10. 15.
5.
0.
0. 3.
15. 2.
0. 11.
8.
0.
0.
4. 12.
0.
0.
8.
8.
0.
0.
5.
8. 0.
0. 9.
8.
0.
0.
4. 11.
0.
1. 12.
7.
0.
0.
2. 14.
5. 1
0. 12.
0. 0.
0.
0.
6. 13. 10.
0.
0.
0.]
print(digits.target)
[0 1 2 ... 8 9 8]
The data is also available at digits.images. This is the raw data of the images in the form of 8 lines and 8
columns.
With "data" an image corresponds to a one-dimensional Numpy array with the length 64, and "images"
representation contains 2-dimensional numpy arrays with the shape (8, 8)
print("Shape of an item: ", digits.data[0].shape)
print("Data type of an item: ", type(digits.data[0]))
print("Shape of an item: ", digits.images[0].shape)
print("Data tpye of an item: ", type(digits.images[0]))
Shape of an item: (64,)
Data type of an item: <class 'numpy.ndarray'>
Shape of an item: (8, 8)
Data tpye of an item: <class 'numpy.ndarray'>
Let's visualize the data:
import matplotlib.pyplot as plt
plt.imshow(digits.images[0], cmap='binary')
plt.show()
270
Let's visualize some more digits combined with their labels:
import matplotlib.pyplot as plt
# set up the figure
fig = plt.figure(figsize=(6, 6)) # figure size in inches
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.0
5, wspace=0.05)
# plot the digits: each image is 8x8 pixels
for i in range(64):
ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolatio
n='nearest')
# label the image with the target value
ax.text(0, 7, str(digits.target[i]))
271
import matplotlib.pyplot as plt
# set up the figure
fig = plt.figure(figsize=(6, 6)) # figure size in inches
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.0
5, wspace=0.05)
# plot the digits: each image is 8x8 pixels
for i in range(144):
ax = fig.add_subplot(12, 12, i + 1, xticks=[], yticks=[])
ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolatio
n='nearest')
# label the image with the target value
#ax.text(0, 7, str(digits.target[i]))
272
from sklearn.model_selection import train_test_split
res = train_test_split(digits.data, digits.target,
train_size=0.8,
test_size=0.2,
random_state=1)
train_data, test_data, train_labels, test_labels = res
from sklearn.neural_network import MLPClassifier
mlp = MLPClassifier(hidden_layer_sizes=(5,),
activation='logistic',
alpha=1e-4,
solver='sgd',
tol=1e-4,
random_state=1,
learning_rate_init=.3,
verbose=True)
273
mlp.fit(train_data, train_labels)
274
Iteration 1, loss = 2.25145782
Iteration 2, loss = 1.97730357
Iteration 3, loss = 1.66620880
Iteration 4, loss = 1.41353830
Iteration 5, loss = 1.29575643
Iteration 6, loss = 1.06663573
Iteration 7, loss = 0.95558862
Iteration 8, loss = 0.94767318
Iteration 9, loss = 0.95242867
Iteration 10, loss = 0.83577430
Iteration 11, loss = 0.74541414
Iteration 12, loss = 0.72011102
Iteration 13, loss = 0.70790928
Iteration 14, loss = 0.69425700
Iteration 15, loss = 0.74458525
Iteration 16, loss = 0.67779333
Iteration 17, loss = 0.69691846
Iteration 18, loss = 0.67844516
Iteration 19, loss = 0.68164743
Iteration 20, loss = 0.68435917
Iteration 21, loss = 0.61988051
Iteration 22, loss = 0.61362164
Iteration 23, loss = 0.56615517
Iteration 24, loss = 0.61323269
Iteration 25, loss = 0.56979209
Iteration 26, loss = 0.58189564
Iteration 27, loss = 0.50692207
Iteration 28, loss = 0.65956191
Iteration 29, loss = 0.53736180
Iteration 30, loss = 0.66437126
Iteration 31, loss = 0.56201738
Iteration 32, loss = 0.85347048
Iteration 33, loss = 0.63673358
Iteration 34, loss = 0.69769079
Iteration 35, loss = 0.62714187
Iteration 36, loss = 0.56914708
Iteration 37, loss = 1.05660379
Iteration 38, loss = 0.66966105
Training loss did not improve more than tol=0.000100utive epochs. Stopping.
for 10 consec
275
Output:MLPClassifier(activation='logistic', alpha=0.0001, batch_siz
e='auto',
beta_1=0.9, beta_2=0.999, early_stopping=False, epsilo
n=1e-08,
hidden_layer_sizes=(5,), learning_rate='constant',
learning_rate_init=0.3, max_iter=200, momentum=0.9,
n_iter_no_change=10, nesterovs_momentum=True, powe
r_t=0.5,
random_state=1, shuffle=True, solver='sgd', tol=0.000
1,
validation_fraction=0.1, verbose=True, warm_start=Fals
e)
predictions = mlp.predict(test_data)
predictions[:25] , test_labels[:25]
Outputarray([1, 5, 0, 7, 7, 0, 6, 1, 5, 4, 9, 2, 7, 8, 4, 1, 7,
3, 7, 4, 7, 4,
8, 6, 0]),
array([1, 5, 0, 7, 1, 0, 6, 1, 5, 4, 9, 2, 7, 8, 4, 6, 9,
3, 7, 4, 7, 1,
8, 6, 0]))
from sklearn.metrics import accuracy_score
accuracy_score(test_labels, predictions)
Output:0.725
for i in range(5, 30):
mlp = MLPClassifier(hidden_layer_sizes=(i,),
activation='logistic',
random_state=1,
alpha=1e-4,
solver='sgd',
tol=1e-4,
learning_rate_init=.3,
verbose=False)
mlp.fit(train_data, train_labels)
predictions = mlp.predict(test_data)
acc_score = accuracy_score(test_labels, predictions)
print(i, acc_score)
276
5 0.725
6 0.37222222222222223
7 0.8166666666666667
8 0.8666666666666667
9 0.8805555555555555
10 0.925
11 0.9388888888888889
12 0.9388888888888889
13 0.9388888888888889
14 0.9527777777777777
15 0.9305555555555556
16 0.95
17 0.8916666666666667
18 0.8638888888888889
/home/bernd/anaconda3/lib/python3.7/site-packages/sklearn/neural_n
etwork/multilayer_perceptron.py:562: ConvergenceWarning: Stochasti
c Optimizer: Maximum iterations (200) reached and the optimizatio
n hasn't converged yet.
% self.max_iter, ConvergenceWarning)
19202122232425262728290.9555555555555556
0.9638888888888889
0.9722222222222222
0.9611111111111111
0.9444444444444444
0.9583333333333334
0.9305555555555556
0.9722222222222222
0.9694444444444444
0.975
0.9611111111111111
In [ ]:
In [ ]:
In [ ]:
In [ ]:
from sklearn.model_selection import GridSearchCV
277
param_grid = [
{
'activation' : ['identity', 'logistic', 'tanh', 'rel
u'],
'solver' : ['lbfgs', 'sgd', 'adam'],
'hidden_layer_sizes': [
(1,),(2,),(3,),(4,),(5,),(6,),(7,),(8,),(9,),(10,),(1
1,), (12,),(13,),(14,),(15,),(16,),(17,),(18,),(19,),(20,),(21,)
]
}
]
In [ ]:
clf = GridSearchCV(MLPClassifier(), param_grid, cv=3,
scoring='accuracy')
clf.fit(train_data, train_labels)
print("Best parameters set found on development set:")
print(clf.best_params_) -
定义
在机器学习中,贝叶斯分类器是一种简单的概率分类器,其基础是应用贝叶斯定理。朴素贝叶斯分类器使用的特征模型做出了强独立性假设。这意味着某个类别特定特征的存在与任何其他特征的存在是独立或不相关的。
独立事件的定义:
如果事件 E 和 F 都具有正概率,并且
且 ,则事件 E 和 F 是独立的。正如我们在定义中所述,朴素贝叶斯分类器基于贝叶斯定理。贝叶斯定理基于条件概率,我们现在将定义它:
条件概率
P(A∣B) 代表“在 B 发生的条件下 A 的条件概率”,或“在条件 B 下 A 的概率”,即在事件 B 发生的前提下,某个事件 A 发生的概率。当在随机实验中已知事件 B 已经发生时,实验的可能结果就减少到 B,因此 A 发生的概率从无条件概率变为在给定 B 条件下的条件概率。联合概率是两个事件同时发生的概率。也就是说,它是两个事件一起发生的概率。A 和 B 的联合概率有三种表示法,可以写成:
-
-
P(AB)
-
P(A,B)
条件概率定义为:
条件概率的例子
讲德语的瑞士人
瑞士约有 840 万人口。其中约 64% 讲德语。地球上约有 75 亿人口。
如果外星人随机传送一个地球人上来,他是讲德语的瑞士人的几率是多少?
我们有以下事件:
S: 成为瑞士人
GS: 讲德语
随机选择一个人是瑞士人的概率:
如果我们知道某人是瑞士人,那么他讲德语的概率是 0.64。这对应于条件概率:
所以地球人是瑞士人并且讲德语的概率可以通过以下公式计算:
代入上述值,我们得到:
从而得到:
所以我们的外星人选中一个讲德语的瑞士人的几率是 0.07168%。
假阳性和假阴性
一个医学研究实验室提议对一大群人进行疾病筛查。反对这种筛查的一个论点是假阳性筛查结果的问题。
假设这组人中有 0.1% 患有该疾病,其余人健康:
和
对于筛查测试,以下情况属实:
如果您患有该疾病,测试将有 99% 的时间呈阳性;如果您没有患病,测试将有 99% 的时间呈阴性:
和
最后,假设当测试应用于患有该疾病的人时,有 1% 的几率出现假阴性结果(和 99% 的几率获得真阳性结果),即:
和
患病
健康
总计
测试结果阳性
99
999
1098
测试结果阴性
1
98901
98902
总计
100
99900
100000
有 999 个假阳性和 1 个假阴性。
问题:
在许多情况下,即使是医疗专业人员也认为“如果你患有这种疾病,测试将在 99% 的时间里呈阳性;如果你没有患病,测试将在 99% 的时间里呈阴性”。在报告阳性结果的 1098 个案例中,只有 99 个(9%)是正确的,而 999 个案例是假阳性(91%),即如果一个人得到阳性测试结果,他或她实际患有该疾病的概率只有大约 9%。
贝叶斯定理
我们计算了条件概率 P(GS∣S),即已知某人是瑞士人的情况下,他或她讲德语的概率。为了计算这个,我们使用了以下等式:
那么计算 P(S∣GS) 又如何呢?即在已知某人讲德语的情况下,他是瑞士人的概率是多少?
这个等式看起来像这样:
让我们在两个等式中都孤立出 P(GS,S):
P(GS,S)=P(GS∣S)P(S)
P(GS,S)=P(S∣GS)P(GS)
由于左侧相等,右侧也必须相等:
这个等式可以转化为:
这个结果对应于贝叶斯定理。
要解决我们的问题——即已知某人讲德语的情况下,他是瑞士人的概率——我们只需计算右侧。我们已经从之前的练习中得知:
和
世界上讲德语的母语者人数约为 1.01 亿,所以我们知道:
最后,我们可以通过将值代入我们的等式来计算 P(S∣GS):
瑞士约有 840 万人口。其中约 64% 讲德语。地球上约有 75 亿人口。
如果外星人随机传送一个地球人上来,他是讲德语的瑞士人的几率是多少?
我们有以下事件:
S: 成为瑞士人 GS: 讲德语
P(A∣B) 是在给定 B 的条件下 A 的条件概率(后验概率),P(B) 是 B 的先验概率,P(A) 是 A 的先验概率。P(B∣A) 是在给定 A 的条件下 B 的条件概率,称为似然。
朴素贝叶斯分类器的一个优点是,它只需要少量训练数据即可估计分类所需的参数。由于假设变量是独立的,因此只需要确定每个类别的变量方差,而不需要确定整个协方差矩阵。
DEFINITION
In machine learning, a Bayes classifier is a simple probabilistic
classifier, which is based on applying Bayes' theorem. The
feature model used by a naive Bayes classifier makes strong
independence assumptions. This means that the existence of a
particular feature of a class is independent or unrelated to the
existence of every other feature.
Definition of independent events:
Two events E and F are independent, if both E and F have
positive probability and if P(E|F) = P(E) and P(F|E) = P(F)
As we have stated in our definition, the Naive Bayes Classifier
is based on the Bayes' theorem. The Bayes theorem is based on
the conditional probability, which we will define now:
CONDITIONAL PROBABILITY
P(A | B) stands for "the conditional probability of A given B", or "the probability of A under the condition B",
i.e. the probability of some event A under the assumption that the event B took place. When in a random
experiment the event B is known to have occurred, the possible outcomes of the experiment are reduced to B,
and hence the probability of the occurrence of A is changed from the unconditional probability into the
conditional probability given B. The Joint probability is the probability of two events in conjunction. That is, it
is the probability of both events together. There are three notations for the joint probability of A and B. It can
be written as
•••P(A ∩ B)
P(AB) or
P(A, B)
The conditional probability is defined by
P(A ∩ B)
P(A | B) =
P(B)
EXAMPLES FOR CONDITIONAL PROBABILITY
GERMAN SWISS SPEAKER
There are about 8.4 million people living in Switzerland. About 64 % of them speak German. There are about
300
7500 million people on earth.
If some aliens randomly beam up an earthling, what are the chances that he is a German speaking Swiss?
We have the events
S: being Swiss
GS: German Speaking
The probability for a randomly chosen person to be Swiss:
8.4
P(S) =
= 0.00112
7500
If we know that somebody is Swiss, the probability of speaking German is 0.64. This corresponds to the
conditional probability
P(GS | S) = 0.64
So the probability of the earthling being Swiss and speaking German, can be calculated by the formula:
P(GS ∩ S)
P(GS | S) =
P(S)
inserting the values from above gives us:
P(GS ∩ S)
0.64 =
0.00112
and
P(GS ∩ S) = 0.0007168
So our aliens end up with a chance of 0.07168 % of getting a German speaking Swiss person.
FALSE POSITIVES AND FALSE NEGATIVES
A medical research lab proposes a screening to test a large group of people for a disease. An argument against
such screenings is the problem of false positive screening results.
Suppose 0,1% of the group suffer from the disease, and the rest is well:
P( " sick " ) = 0, 1
and
301
P( " well " ) = 99, 9
The following is true for a screening test:
If you have the disease, the test will be positive 99% of the time, and if you don't have it, the test will be
negative 99% of the time:
P("test positive" | "well") = 1 %
and
P("test negative" | "well") = 99 %.
Finally, suppose that when the test is applied to a person having the disease, there is a 1% chance of a false
negative result (and 99% chance of getting a true positive result), i.e.
P("test negative" | "sick") = 1 %
and
P("test positive" | "sick") = 99 %
Sick
Healthy
Totals
Test result positive
99
999
1098
Test result
1
98901
98902
negative
Totals
100
99900
100000
There are 999 False Positives and 1 False Negative.
Problem:
In many cases even medical professionals assume that "if you have this sickness, the test will be positive in 99
% of the time and if you don't have it, the test will be negative 99 % of the time. Out of the 1098 cases that
report positive results only 99 (9 %) cases are correct and 999 cases are false positives (91 %), i.e. if a person
gets a positive test result, the probability that he or she actually has the disease is just about 9 %. P("sick" |
"test positive") = 99 / 1098 = 9.02 %
BAYES' THEOREM
We calculated the conditional probability P(GS | S), which was the probability that a person speaks German, if
302
he or she is known to be Swiss. To calculate this we used the following equation:
P(GS, S)
P(GS | S) =
P(S)
What about calculating the probability P(S | GS), i.e. the probability that somebody is Swiss under the
assumption that the person speeks German?
The equation looks like this:
P(GS, S)
P(S | GS) =
P(GS)
Let's isolate on both equations P(GS, S):
P(GS, S) = P(GS | S)P(S)
P(GS, S) = P(S | GS)P(GS)
As the left sides are equal, the right sides have to be equal as well:
P(GS | S) ∗ P(S) = P(S | GS)P(GS)
This equation can be transformed into:
P(GS | S)P(S)
P(S | GS) =
P(GS)
The result corresponts to Bayes' theorem
To solve our problem, - i.e. the probability that a person is Swiss, if we know that he or she speaks German -
all we have to do is calculate the right side. We know already from our previous exercise that
P(GS | S) = 0.64
and
P(S) = 0.00112
The number of German native speakers in the world corresponds to 101 millions, so we know that
101
P(GS) =
= 0.0134667
7500
Finally, we can calculate P(S | GS) by substituting the values in our equation:
P(GS | S)P(S)
0.64 ∗ 0.00112
P(S | GS) =
=
= 0.0532276
P(GS)
0.0134667
303
There are about 8.4 million people living in Switzerland. About 64 % of them speak German. There are about
7500 million people on earth.
If the some aliens randomly beam up an earthling, what are the chances that he is a German speaking Swiss?
We have the events
S: being Swiss GS: German Speaking
8.4
P(S) =
= 0.00112
7500
P(B | A)P(A)
P(A | B) =
P(B)
P(A | B) is the conditional probability of A, given B (posterior probability), P(B) is the prior probability of B
and P(A) the prior probability of A. P(B | A) is the conditional probability of B given A, called the likely-hood.
An advantage of the naive Bayes classifier is that it requires only a small amount of training data to estimate
the parameters necessary for classification. Because independent variables are assumed, only the variances of
the variables for each class need to be determined and not the entire covariance matrix. -
-
引言 (INTRODUCTION)
在前一章关于分类决策树的内容中,我们介绍了决策树模型的基本概念、如何从零开始用 Python 构建它们,以及如何使用
sklearn
预打包的DecisionTreeClassifier
方法。我们还介绍了决策树模型的优点和缺点,以及重要的扩展和变体。分类决策树的一个缺点是它们需要一个分类尺度的目标特征,例如weather = {Sunny, Rainy, Overcast, Thunderstorm}
。这里出现了一个问题:如果我们的树想要预测房子的价格,给定一些目标特征属性,如房间数量和位置,该怎么办?在这种情况下,目标特征(价格)的值不再是分类尺度的,而是连续的——理论上,一栋房子可以有无限多种不同的价格。
这就是回归树的作用。回归树的原理与分类树相同,但最大的区别在于目标特征值现在可以取无限多个连续尺度的值。因此,现在的任务是预测连续尺度的目标特征 Y 的值,给定一组分类(或连续)尺度的描述性特征 X 的值。
如上所述,构建回归树的原理与创建分类树的方法相同。
我们寻找能最纯粹地分割目标特征值的描述性特征,沿着该描述性特征的值分割数据集,并对每个子数据集重复此过程,直到达到停止标准。如果我们达到停止标准,我们就生成一个叶节点。
然而,有几点发生了变化。
首先,让我们考虑在分类树章节中介绍的用于生成叶节点的停止标准:
-
如果分裂过程导致数据集为空,返回原始数据集的众数目标特征值。
-
如果分裂过程导致数据集中没有剩余特征,返回直接父节点的众数目标特征值。
-
如果分裂过程导致数据集中的目标特征值是纯的,返回该值。
如果我们现在考虑我们新的连续尺度目标特征的属性,我们会发现第三个停止标准不能再使用了,因为目标特征值现在可以取无限多个不同的值。因此,我们极不可能找到纯粹的目标特征值,除非数据集中只剩下一个实例。
简而言之,通常没有纯粹的目标特征值这种东西。
为了解决这个问题,我们将引入一个早期停止标准:如果数据集中实例的数量小于或等于 5,则返回数据集中剩余目标特征值的平均值。
一般来说,在处理回归树时,我们将在叶节点处返回平均目标特征值作为预测。
我们必须进行的第二个改变在考虑分裂过程本身时变得显而易见。
在使用分类树时,我们使用特征的信息增益 (IG) 作为分裂标准。也就是说,具有最大信息增益的特征被用来分割数据集。考虑以下示例,我们只检查一个描述性特征,比如卧室数量,以及房屋成本作为目标特征。
Pythonimport pandas as pd import numpy as np df = pd.DataFrame({'Number_of_Bedrooms':[2,2,4,1,3,1,4,2],'Price_of_Sale':[100000,120000,250000,80000,220000,170000,500000,75000]}) print(df)
Output:
Number_of_Bedrooms Price_of_Sale 0 2 100000 1 2 120000 2 4 250000 3 1 80000 4 3 220000 5 1 170000 6 4 500000 7 2 75000
那么我们如何计算
Number_of_Bedrooms
特征的熵呢?如果我们计算加权熵,我们会看到对于
,我们得到的加权熵为 0。我们得到这个结果是因为数据集中只有一栋房子有 3 间卧室。另一方面,对于 (出现三次),我们将得到 0.59436 的加权熵。简而言之,由于我们的目标特征是连续尺度的,分类尺度描述性特征的信息增益不再是合适的分裂标准。
嗯,我们可以转而根据目标特征的值对其进行分类,例如,将房价在 0 美元到 80000 美元之间归类为“低”,80001 美元到 150000 美元之间归类为“中”,150001 美元以上归类为“高”。
我们在这里所做的,是将我们的回归问题转换成了某种分类问题。然而,由于我们希望能够从无限多的可能值(回归)中进行预测,这并不是我们正在寻找的。
让我们回到最初的问题:我们希望有一个分裂标准,它能够以这样一种方式分割数据集:当到达一个树节点时,预测值(我们将预测值定义为该叶节点处实例的平均目标特征值,其中我们将最少 5 个实例定义为早期停止标准)最接近实际值。
事实证明,方差是回归树最常用的分裂标准之一,我们将使用方差作为分裂标准。
这样做的解释是,我们希望寻找那些在沿这些目标特征值分割数据集时,最精确地指向真实目标特征值的特征属性。因此,请看下面的图片。您认为
Number_of_Bedrooms
特征的这两种布局中,哪一种能更精确地指向真实销售价格?嗯,显然是方差最小的那个!我们将在下一节介绍方差度量背后的数学原理。
目前,我们首先用箭头来表示这些,其中宽箭头表示高方差,细箭头表示低方差。我们可以通过显示描述性特征的每个值对应的目标特征的方差来阐明这一点。正如您所看到的,当我们在沿描述性特征的值分割数据集时,最小化目标特征值方差的特征布局是最精确地指向真实值的特征布局,因此应该用作分裂标准。在创建我们的回归树模型时,我们将使用方差度量来取代信息增益作为分裂标准。
In the previous chapter about
Classification decision Trees we have
introduced the basic concepts underlying
decision tree models, how they can be
build with Python from scratch as well as
using the prepackaged sklearn
DecisionTreeClassifier method. We have
also introduced advantages and
disadvantages of decision tree models as
well as important extensions and
variations. One disadvantage of
Classification decision Trees is that they
need a target feature which is
categorically scaled like for instance
weather = {Sunny, Rainy, Overcast,
Thunderstorm}.
Here arises a problem: What if we want our tree for instance to predict the price of a house given some target
feature attributes like the number of rooms and the location? Here the values of the target feature (prize) are no
longer categorically scaled but are continuous - A house can have, theoretically, a infinite number of different
prices -
Thats where Regression Trees come in. Regression Trees work in principal in the same way as Classification
Trees with the large difference that the target feature values can now take on an infinite number of
continuously scaled values. Hence the task is now to predict the value of a continuously scaled target feature Y
given the values of a set of categorically (or continuously) scaled descriptive features X.
413
As stated above, the principle of building a Regression Tree follows the same approach as the creation of a
Classification Tree.
We search for the descriptive feature which splits the target feature values most purely, divide the dataset
along the values of this descriptive feature and repeat this process for each of the sub datasets until we
accomplish a stopping criteria.If we accomplish a stopping criteria, we grow a leaf node.
Though, a few things changed.
First of all, let us consider the stopping criteria we have introduced in the Classification Tree chapter to grow a
leaf node:
1.2.3.If the splitting process leads to a empty dataset, return the mode target feature value of the
original dataset
If the splitting process leads to a dataset where no features are left, return the mode target feature
value of the direct parent node
If the splitting process leads to a dataset where the target feature values are pure, return this
value
If we now consider the property of our new continuously scaled target feature we mention that the third
stopping criteria can no longer be used since the target feature values can now take on an infinite number of
different values. Consequently, it is most likely that we will not find pure target feature values until there is
only one instance left in the dataset.
To make a long story short, there is in general nothing like pure target feature values.
To address this issue, we will introduce an early stopping criteria that returns the average value of the target
feature values left in the dataset if the number of instances in the dataset is ≤ 5.
In general, while handling with Regression Trees we will return the average target feature values as prediction
at a leaf node.
The second change we have to make becomes apparent when we consider the splitting process itself.
While working with Classification Trees we used the Information Gain (IG) of a feature as splitting criteria.
That is, the feature with the largest IG was used to split the dataset on. Consider the following example where
we examine only one descriptive feature, lets say the number of bedrooms, and the costs of the house as target
feature.
import pandas as pd
import numpy as np
df = pd.DataFrame({'Number_of_Bedrooms':[2,2,4,1,3,1,4,2],'Price_o
f_Sale':[100000,120000,250000,80000,220000,170000,500000,75000]})
df
414
Output:
Number_of_Bedrooms
Price_of_Sale
0
2
100000
1
2
120000
2
4
250000
3
1
80000
4
3
220000
5
1
170000
6
4
500000
7
2
75000
Now how would we calculate the entropy of the Number_of_Bedrooms feature?
| D |
Number of Bedrooms = jH(Number of Bedrooms) = ∑j ∈ Number of Bedrooms ∗ (
| D |
∗ ( ∑ k ∈ Price of Sale ∗ ( − P(k | j) ∗ log2(P(k | j))
If we calculate the weighted entropies, we see that for j = 3, we get a weighted entropy of 0. We get this result
because there is only one house in the dataset with 3 bedrooms. On the other hand, for j = 2 (occurs three
times) we will get a weighted entropy of 0.59436.
To make a long story short, since our target feature is continuously scaled, the IGs of the categorically scaled
descriptive features are no longer appropriate splitting criteria.
Well, we could instead categorize the target feature along its values where for instance housing prices between
$0 and $80000 are categorized as low, between $80001 and $150000 as middle and > $150001
as high.
What we have done here is converting our regression problem into kind of a classification problem. Though,
since we want to be able to make predictions from a infinite number of possible values (regression) this is not
what we are looking for.
Lets come back to our initial issue: We want to have a splitting criteria which allows us to split the dataset in
such a way that when arriving a tree node, the predicted value (we defined the predicted value as the mean
target feature value of the instances at this leaf node where we defined the minimum number of 5 instances as
early stopping criteria) is closest to the actual value.
It turns out that the variance is one of the most commonly used splitting criteria for regression trees where we
will use the variance as splitting criteria.
The explanation therefore is, that we want to search for the feature attributes which most exactly point to the
415
real target feature values when splitting the dataset along the values of these target features. Therefore,
examine the following picture. What do you think which of those two layouts of the Number_of_Bedrooms
feature points more exactly to the real sales prize?
Well, obviously that one with the smallest variance! We will introduce the maths behind the measure of
variance in the next section.
For the time being we start by illustrating these by arrows where wide arrows represent a high variance and
slim arrows a low variance. We can illustrate that by showing the variance of the target feature for each value
of the descriptive feature. As you can see, the feature layout which minimizes the variance of the target feature
values when we split the dataset along the values of the descriptive feature is the feature layout which most
416
exactly points to the real value and hence should be used as splitting criteria. During the creation of our
Regression Tree model we will use the measure of variance to replace the information gain as splitting criteria. -
-
如上所述,构建回归树的任务原则上与创建分类树相同。然而,由于目标特征的连续性,信息增益(IG)不再是合适的分裂标准(基尼指数也不适用),因此我们必须采用新的分裂标准。为此,我们将引入方差。
方差 (Variance)
其中 y_i 是单个目标特征值,bary 是这些目标特征值的平均值。
以上面为例,
= \(19.903125 \times 10^9 \quad \text{# 很大的数字 ;) \text{但这不影响我们的计算}\)Price_of_Sale
目标特征的总方差计算如下:由于我们想知道哪个描述性特征最适合用于分割目标特征,我们必须计算描述性特征的每个值相对于目标特征值的方差。
因此,对于上面的
Number_of_Rooms
描述性特征,我们得到单个房间数量的方差:
由于我们现在还想解决某些特征值出现频率相对较低但方差较高的问题(这可能导致整个特征的方差非常高,仅仅因为一个离群的特征值,即使所有其他特征值的方差可能很小),我们通过计算每个特征值的加权方差来解决这个问题:
最后,我们将这些加权方差相加,以对整个特征进行评估:
在我们的例子中是:
1012500000+190625000+0+7812500000=9015625000
将所有这些结合起来,最终得到了我们将用于分裂过程中每个节点以确定下一个应该选择哪个特征来分割数据集的加权特征方差公式。
这里,f 表示单个特征,l 表示特征的值(例如
Price == medium
),t 表示子集中目标特征的值,其中 。
按照这个计算规范,我们可以在每个节点找到要分割数据集的特征。
为了说明沿最低方差特征值分割数据集的过程,我们以 UCI 自行车共享数据集的简化示例为例,我们将在本章的“从头开始用 Python 实现回归树”部分中使用该数据集,并计算每个特征的方差以找到我们应该用作根节点的特征。
Pythonimport pandas as pd df = pd.read_csv("data/day.csv", usecols=['season','holiday','weekday','weathersit','cnt']) df_example = df.sample(frac=0.012) # 以下是示例计算,实际输出会依赖于 df_example 的随机样本 # 假设 df_example 包含以下数据,以便于理解计算过程 # season: 1, 1, 2, 3, 3, 4, 4, 4, 4 # cnt: 352, 421, 12, 162, 112, 161, 109, 79, 79 # 这里给出的计算是加权方差的近似值,具体值取决于df_example中的实际数据 # 为了演示概念,以下是根据原文提供的输出格式进行的推断: # Season (假设 Season 有4个唯一值,以及它们对应的cnt均值和数据点数量) # 例如: # Season 1: cnts = [352, 421], mean = 386.5 # Season 2: cnts = [12], mean = 12 # Season 3: cnts = [162, 112], mean = 137 # Season 4: cnts = [161, 109, 79, 79], mean = 107 # n (total samples) = 9 # WeightVar(Season) = (2/9) * Var(Season=1_cnts) + (1/9) * Var(Season=2_cnts) + ... # 原文提供了一个简化值: # WeightVar(Season) = 16429.1 # Weekday (假设 Weekday 有多个唯一值,以及它们对应的cnt均值和数据点数量) # 例如: # Weekday 1: cnts = [109, 79], mean = 94 # Weekday 2: cnts = [162, 112], mean = 137 # Weekday 3: (no instance in example) # Weekday 4: cnts = [421], mean = 421 # Weekday 5: cnts = [161], mean = 161 # n (total samples) = 9 # WeightVar(Weekday) = (2/9) * Var(Weekday=1_cnts) + (2/9) * Var(Weekday=2_cnts) + ... # 原文提供了一个计算格式,没有最终值,但假设它会很小 # Weathersit (假设 Weathersit 有多个唯一值,以及它们对应的cnt均值和数据点数量) # 例如: # Weathersit 1: cnts = [421, 165, 12, 161, 112], mean = 174.2 # Weathersit 2: cnts = [352, 109], mean = 230.5 # n (total samples) = 9 # WeightVar(Weathersit) = (5/9) * Var(Weathersit=1_cnts) + (2/9) * Var(Weathersit=2_cnts) + ... # 原文提供了一个计算格式,没有最终值,但假设它会比较大
由于
Weekday
特征的方差最低,因此该特征用于分割数据集,并因此充当根节点。尽管由于随机抽样,此示例不够健壮(例如,没有weekday == 3
的实例),但它应该传达使用方差作为分裂度量的数据分割背后的概念。
既然我们已经介绍了如何使用方差度量来分割具有连续目标特征的数据集的概念,我们现在将调整分类树的伪代码,以便我们的树模型能够处理连续尺度的目标特征值。
如上所述,我们需要进行两项更改,以使我们的树模型能够处理连续尺度的目标特征值:
1. 我们引入一个早期停止标准**,规定如果节点处的实例数量小于或等于 5(我们可以调整此值),则返回这些数字的平均目标特征值。**
2. 我们不再使用信息增益**,而是使用特征的方差作为新的分裂标准。**
因此,伪代码变为:
ID3(D, Feature_Attributes, Target_Attributes, min_instances=5) 创建根节点 r 将 r 设置为 D 中目标特征值的**平均值** ####### 已更改 ######## 如果 num_instances <= min_instances : 返回 r 否则: 继续 如果 Feature_Attributes 为空: 返回 r 否则: Att = Feature_Attributes 中**加权方差最低**的属性 ####### 已更改 ######## r = Att 对于 Att 中的每个值: 在 r 下添加一个新节点,其中 node_values = (Att == values) Sub_D_values = (Att == values) 如果 Sub_D_values == empty: 添加一个叶节点 l,其中 l 等于 D 中目标值的**平均值** 否则: 添加子树,使用 ID3(Sub_D_values, Feature_Attributes = Feature_Attributes 中去除 Att 的属性, Target_Attributes, min_instances=5)
除了实际算法中的更改,我们还必须使用另一种准确性度量,因为我们不再处理分类目标特征值。也就是说,我们不能再简单地将预测的类别与真实的类别进行比较,并计算命中目标的百分比。相反,我们使用均方根误差 (RMSE) 来衡量我们模型的“准确性”。
RMSE 的公式为:
其中 \(t_i\) 是测试数据集的实际测试目标特征值,Model(\(test_i\)) 是我们训练的回归树模型对这些 \(t_i\) 预测的值。通常,RMSE 值越低,我们的模型越符合实际数据。
既然我们已经调整了我们主要的 ID3 分类树算法以处理连续尺度的目标特征,并将其转变为回归树模型,我们就可以开始在 Python 中实现这些更改了。
因此,我们只需从前一章中获取分类树模型,并实现上述两项更改。
As stated above, the task during growing a Regression Tree is in principle the same as during the creation of
Classification Trees. Though, since the IG turned out to be no longer an appropriate splitting criteria (neither is
the Gini Index) due to the continuous character of the target feature we must have a new splitting criteria.
Therefore we use the variance which we will introduce now.
Variance
∑ n
( y − y ˉ )
ii =1
Var(x) =
n −1
Where y are the single target feature values and y ˉ is the mean of these target feature values.
iTaking the example from above the total variance of the Prize_of_Sale target feature is calculated with:
( 100000 − 189375 )2 + ( 120000 − 189375 ) 2 + ( 250000 − 189375 ) 2 + ( 80000 − 189375 )2 + ( 220000 − 189375 ) 2 + ( 170000 − 18
Var(Price of Sale) =
7
= 19.903125 ∗ 10 9 #Large Number ;) Though this has no effect on our calculations
Since we want to know which descriptive feature is best suited to split the target feature on, we have to
calculate the variance for each value of the descriptive feature with respect to the target feature values.
Hence for the Number_of_Rooms descriptive feature above we get for the single numbers of rooms:
( 80000 − 125000 )2 + ( 170000 − 125000 )2
Var(Number of Rooms = 1) =
= 4050000000
1
( 100000 − 98333.3 ) 2 + ( 120000 − 98333.3 ) 2 + ( 75000 − 98333.3 ) 2
Var(Number of Rooms = 2) =
= 508333333.3
2
Var(Number of Rooms = 3) = (220000 − 220000) 2 = 0
( 250000 − 375000 ) 2 + ( 500000 − 375000 ) 2
Var(Number of Rooms = 4) =
= 31250000000
1
Since we now want to also address the issue that there are feature values which occur relatively rarely but
have a high variance (This could lead to a very high variance for the whole feature just because of one outliner
feature value even though the variance of all other feature values may be small) we address this by calculating
the weighted variance for each feature value with:
2
WeightVar(Number of Rooms = 1) = ∗ 4050000000 = 1012500000
8418
2
WeightVar(Number of Rooms = 2) = ∗ 508333333.3 = 190625000
82
WeightVar(Number of Rooms = 3) = ∗ 0 = 0
82
WeightVar(Number of Rooms = 4) = ∗ 31250000000 = 7812500000
8Finally, we sum up these weighted variances to make an assessment about the feature as a whole:
SumVar(feature) = ∑ value ∈ featureWeightVar(feature value)
Which is in our case:
1012500000 + 190625000 + 0 + 7812500000 = 9015625000
Putting all this together finally leads to the formula for the weighted feature variance which we will use at
each node in the splitting process to determine which feature we should choose to split our dataset on next.
| f = l |
feature[choose] = argminf ∈ features ∑ l ∈ levels ( f ) | f | ∗ Var(t, f = l)
∑n
( t − t ˉ ) 2
| f = l |
i = 1
i= argminf ∈ features ∑ l ∈ levels ( f ) | f | ∗
n −1
Here f denotes a single feature, l denotes the value of a feature (e.g Price == medium), t denotes the value of
the target feature in the subset where f=l.
Following this calculation specification we find the feature at each node to split our dataset on.
419
To illustrate the process of splitting the dataset along the feature values of the lowest variance feature, we take
a simplified example of the UCI bike sharing dataset which we will use later on in the Regression Trees from
scratch with Python part of this chapter and calculate the variance for each feature to find the feature we
should use as root node.
import pandas as pd
df = pd.read_csv("data/day.csv",usecols=['season','holiday','weekd
ay','weathersit','cnt'])
df_example = df.sample(frac=0.012)
Season
1
5
( 352 − 211.8 ) 2 + ( 421 − 211.8 )2 + ( 12 − 211.8 )2 + ( 162 − 211.8 ) 2 + ( 112 − 211.8 ) 2
1
WeightVar(Season) = ∗ (79 − 79) 2 + ∗
+ ∗ (161
9 9 4
9= 16429.1
Weekday
2
( 109 − 94 ) 2 + ( 79 − 94 ) 2
2
( 162 − 137 )2 + ( 112 − 137 )2
1
2
( 161 − 86.5 ) 2 + ( 12
WeightVar(Weekday) = ∗
+ ∗
+ ∗ (421 − 421) 2 +
∗
9 1
9 1
9
9
1
Weathersit
4
( 421 − 174.2 )2 + ( 165 − 174.2 )2 + ( 12 − 174.2 ) 2 + ( 161 − 174.2 ) 2 + ( 112 − 174.2 ) 2
2
( 352 − 230.5 ) 2 + ( 109
WeightVar(Weathersit) = ∗
+ ∗
9 4
9 1
Since the Weekday feature has the lowest variance, this feature is used to split the dataset on and hence serves
as root node. Though due to random sampling, this example is not that robust (for instance there is no instance
420
with weekday == 3) it should convey the concept behind the data splitting using variance as splitting measure.
Since we now have introduced the concept of how the
measure of variance can be used to split a dataset with a
continuous target feature, we will now adapt the pseudocode
for Classification Trees such that our tree model is able to
handle continuously scaled target feature values.
As stated above, there are two changes we have to make to
enable our tree model to handle continuously scaled target
feature values:
**1. We introduce an early stopping criteria where we say
that if the number of instances at a node is ≤ 5 (we can
adjust this value), return the mean target feature value of
these numbers**
**2. Instead of the information gain we use the variance of a
feature as our new splitting criteria**
Hence the pseudocode becomes:
ID3(D,Feature_Attributes,Target_Attr
ibutes,min_instances=5)
Create a root node r
Set r to the mean of the target feature values in D #######Cha
nged########
If num_instances <= min_instances :
return r
Else:
pass
If Feature_Attributes is empty:
return r
Else:
Att = Attribute from Feature_Attributes with the lowest we
ighted variance ########Changed########
r = Att
For values in Att:
Add a new node below r where node_values = (Att == val
ues)
421
Sub_D_values = (Att == values)
If Sub_D_values == empty:
Add a leaf node l where l equals the mean of the t
arget values in D
Else:
add Sub_Tree with ID3(Sub_D_values,Feature_Attribu
tes = Feature_Attributes without Att, Target_Attributes,min_instan
ces=5)
In addition to the changes in the actual algorithm we also have to use another measure of accuracy because we
are no longer dealing with categorical target feature values. That is, we can no longer simply compare the
predicted classes with the real classes and calculate the percentage where we bang on the target. Instead we are
using the root mean square error (RMSE) to measure the "accuracy" of our model.
The equation for the RMSE is:
RMSE =
√
∑ n
i = i
( t i − Model n
( test i ) )2
Where t i are the actual test target feature values of a test dataset and Model(test i) are the values predicted by
our trained regression tree model for these t i. In general, the lower the RMSE value, the better our model fits
the actual data.
Since we now have adapted our principal ID3 classification tree algorithm to handle continuously scaled target
features and therewith have made it to a regression tree model, we can start implementing these changes in
Python.
Therefore we simply take the classification tree model from the previous chapter and implement the two
changes mentioned above. -
正如我们之前宣布的,为了实现我们的回归树模型,我们将使用 UCI 自行车共享数据集。我们将使用全部 731 个实例以及原始 16 个属性的子集。我们使用的属性是以下特征:
{'season', 'holiday', 'weekday', 'workingday', 'weathersit', 'cnt'}
,其中{'cnt'}
特征作为我们的目标特征,表示每天租用自行车的总数。数据集的前五行如下所示:
Pythonimport pandas as pd dataset = pd.read_csv("data/day.csv",usecols=['season','holiday','weekday','workingday','weathersit','cnt']) dataset.sample(frac=1).head()
Output:
season holiday weekday workingday weathersit cnt 458 2 0 2 1 1 6772 245 3 0 6 0 1 4484 86 2 0 1 1 1 2028 333 4 0 3 1 1 3613 507 2 0 2 1 2 6073
现在我们将开始修改原始创建的分类算法。有关代码的进一步注释,请读者参考前面关于分类树的章节。
所需 Python 包的导入 (Imports of Python Packages Needed)
Pythonimport pandas as pd import numpy as np from pprint import pprint import matplotlib.pyplot as plt from matplotlib import style style.use("fivethirtyeight") # 导入数据集并定义特征和目标列 dataset = pd.read_csv("data/day.csv",usecols=['season','holiday','weekday','workingday','weathersit','cnt']).sample(frac=1) mean_data = np.mean(dataset.iloc[:,-1])
方差计算函数 (Calculate the Variance of a Dataset)
Python""" 计算数据集的方差。 此函数接受三个参数。 1. data = 应该计算其特征方差的数据集 2. split_attribute_name = 应该计算其加权方差的特征名称 3. target_name = 目标特征的名称。此示例默认为 "cnt" """ def var(data,split_attribute_name,target_name="cnt"): feature_values = np.unique(data[split_attribute_name]) feature_variance = 0 for value in feature_values: # 创建数据子集 --> 沿 split_attribute_name 特征的值分割原始数据 # 并重置索引,以避免在使用 df.loc[] 操作时出现错误 subset = data.query('{0}=={1}'.format(split_attribute_name,value)).reset_index() # 计算每个子集的加权方差 # ddof=1 用于计算样本方差,即除以 n-1 value_var = (len(subset)/len(data))*np.var(subset[target_name],ddof=1) # 计算特征的加权方差 feature_variance+=value_var return feature_variance
分类算法 (Classification Algorithm)
Pythondef Classification(data,originaldata,features,min_instances,target_attribute_name,parent_node_class = None): """ 分类算法:此函数接受与前一章中原始分类算法相同的 5 个参数, 外加一个参数 (min_instances),它定义了每个节点的最小实例数作为早期停止标准。 """ # 定义停止条件 --> 如果满足其中一个条件,我们希望返回一个叶节点 ######### 此条件是新的 ######### # 如果所有 target_values 具有相同的值(对于分类),此处应返回目标特征的平均值 if len(data) <= int(min_instances): return np.mean(data[target_attribute_name]) ####################################################### # 如果数据集为空,返回原始数据集中的目标特征平均值 elif len(data)==0: return np.mean(originaldata[target_attribute_name]) # 如果特征空间为空,返回直接父节点的平均目标特征值 --> 注意,直接父节点是调用当前算法运行的节点, # 因此平均目标特征值存储在 parent_node_class 变量中。 elif len(features) ==0: return parent_node_class # 如果以上条件均不成立,则生长树! else: # 设置此节点的默认值 --> 当前节点的平均目标特征值 parent_node_class = np.mean(data[target_attribute_name]) # 选择最能分割数据集的特征 item_values = [var(data,feature) for feature in features] # 返回数据集中特征的方差 best_feature_index = np.argmin(item_values) best_feature = features[best_feature_index] # 创建树结构。根节点获取方差最小的特征 (best_feature) 的名称。 tree = {best_feature:{}} # 从特征空间中移除方差最小的特征 features = [i for i in features if i != best_feature] # 为根节点特征的每个可能值生长一个分支 for value in np.unique(data[best_feature]): value = value # 沿方差最小的特征值分割数据集,从而创建子数据集 sub_data = data.where(data[best_feature] == value).dropna() # 使用新参数为每个子数据集调用分类算法 --> 递归在此处体现! subtree = Classification(sub_data,originaldata,features,min_instances,'cnt',parent_node_class = parent_node_class) # 将从子数据集生长的子树添加到根节点下的树中 tree[best_feature][value] = subtree return tree
预测查询实例 (Predict Query Instances)
Pythondef predict(query,tree,default = mean_data): for key in list(query.keys()): if key in list(tree.keys()): try: result = tree[key][query[key]] except KeyError: # 捕获 KeyError 以处理未在训练数据中出现的新值 return default # 这里的 result = tree[key][query[key]] 是重复的,可以删除一个 # result = tree[key][query[key]] if isinstance(result,dict): return predict(query,result) else: return result
创建训练集和测试集 (Create Training and Testing Set)
Pythondef train_test_split(dataset): # 我们丢弃索引并重新标记索引,从 0 开始,因为我们不想在行标签/索引方面遇到错误 training_data = dataset.iloc[:int(0.7*len(dataset))].reset_index(drop=True) testing_data = dataset.iloc[int(0.7*len(dataset)):].reset_index(drop=True) return training_data,testing_data training_data = train_test_split(dataset)[0] testing_data = train_test_split(dataset)[1]
计算 RMSE (Compute the RMSE)
Pythondef test(data,tree): # 通过简单地从原始数据集中删除目标特征列并将其转换为字典来创建新的查询实例 queries = data.iloc[:,:-1].to_dict(orient = "records") # 创建一个空 DataFrame,其列中存储树的预测 predicted = [] # 计算 RMSE for i in range(len(data)): predicted.append(predict(queries[i],tree,mean_data)) RMSE = np.sqrt(np.sum(((data.iloc[:,-1]-predicted)**2)/len(data))) return RMSE
训练树、打印树和预测准确性 (Train the Tree, Print the Tree and Predict the Accuracy)
Pythontree = Classification(training_data,training_data,training_data.columns[:-1],5,'cnt') pprint(tree) print('#'*50) print('Root mean square error (RMSE): ',test(testing_data,tree))
Output:
{'season': {1: {'weathersit': {1.0: {'workingday': {0.0: {'holiday': {0.0: {0.0: 2398.1071428571427, 6.0: 2398.1071428571427}}, {1.0: 3284.28, 1.0: 1.0: {'holiday': {0.0: 2.0: 3284.28, 3.0: 3284.28, 4.0: 3284.28, 5.0: 3284.28}}}}}}, 2.0: {'holiday': {0.0: {'weekday': {0.0: 2581.0: 21865, 2.0: {'w {1.0: 2140.6666666666665}}, 3.0: {'w {1.0: 2049.0}}, 4.0: {'w {1.0: 3105.714285714286}}, 5.0: {'w {1.0: 2844.5454545454545}}, 6.0: {'w {0.0: 1757.111111111111}}}}, 1.0: 1040.0}}, 3.0: 473.5}}, 2: {'weathersit': {1.0: {'workingday': {0.0: {'weekday': {0.0: {0.0: 5728.2}}, 1.0: 6667, 5.0: 6.0: {0.0: 6206.142857142857}}}}, 1.0: {'holiday': {0.0: {1.0: 5340.06, 2.0:3.0:4.0:5340.06, 5340.06, 5340.06, 5.0: 5340.06}}}}}}, 2.0: {'holiday': {0.0: {'workingday': {0.0: {0.0: 4737.0, 6.0: 4349.7692307692305}}, 1.0: {1.0: 4446.294117647059, 2.0: 4446.294117647059, 3.0: 4446.294117647059, 4.0: 4446.294117647059, 5.0: 5975.333333333333}}}}}}, 3.0: 1169.0}}, 3: {'weathersit': {1.0: {'holiday': {0.0: {'workingday': {0.0: {0.0: 5715.0, 6.0: 5715.0}}, 1.0: {1.0: 6148.342857142857, 2.0: 6148.342857142857, 3.0: 6148.342857142857, 4.0: 6148.342857142857, 5.0: 6148.342857142857}}}}, 1.0: 7403.0}}, 2.0: {'workingday': {0.0: {'holiday': {0.0: {0.0: 4537.5, 6.0: 5028.8}}, 1.0: 1.0: {'holiday': {0.0: {1.0: 6745.25, 2.0: 5222.4, 3.0: 5554.0, 4.0: 4580.0, 5.0: 5389.409090909091}}}}}}, 3.0: 2276.0}}, 4: {'weathersit': {1.0: {'holiday': {0.0: {'workingday': {0.0: {0.0: 4974.772727272727, 6.0: 4974.772727272727}}, {1.0: 5174.906976744186, 1.0: 2.0: 5174.906976744186, 3.0: 5174.906976744186, 4.0: 5174.906976744186, 5.0: 5174.906976744186}}}}, 1.0: 3101.25}}, 2.0: {'weekday': {0.0: 3795.6666666666665, 1.0: 4536.0, 2.0: {'holiday': {0.0: {'w {1.0: 4440.875}}}}, 3.0: 5446.4, 4.0: 5888.4, 5.0: 5773.6, 6.0: 4215.8}}, 3.0: {'weekday': {1.0: 1393.5, 2.0: 2946.6666666666665, 3.0: 1840.5, 6.0: 627.0}}}}}} ################################################## Root mean square error (RMSE): 1623.9891244058906
RMSE 学习曲线绘制 (Plot the RMSE with Respect to the Minimum Number of Instances)
上面我们可以看到每个节点最少 5 个实例时的 RMSE。但目前,我们不知道这个结果是好是坏。为了了解我们模型的“准确性”,我们可以绘制一种学习曲线,其中我们绘制最小实例数与 RMSE 的关系。
Python""" 绘制 RMSE 与最小实例数的关系 """ fig = plt.figure() ax0 = fig.add_subplot(111) RMSE_test = [] RMSE_train = [] for i in range(1,100): tree = Classification(training_data,training_data,training_data.columns[:-1],i,'cnt') RMSE_test.append(test(testing_data,tree)) RMSE_train.append(test(training_data,tree)) ax0.plot(range(1,100),RMSE_test,label='Test_Data') ax0.plot(range(1,100),RMSE_train,label='Train_Data') ax0.legend() ax0.set_title('RMSE with respect to the minimum number of instances per node') ax0.set_xlabel('#Instances') ax0.set_ylabel('RMSE') plt.show()
最终回归树模型 (Final Regression Tree Model)
正如我们所看到的,增加每个节点的最小实例数会导致我们的测试数据的 RMSE 降低,直到我们达到大约每个节点 50 个实例。在这里,
Test_Data
曲线趋于平坦,并且叶子中最小实例数的额外增加并不会显著降低我们测试集的 RMSE。让我们绘制一个最小实例数为 50 的树。
Pythontree = Classification(training_data,training_data,training_data.columns[:-1],50,'cnt') pprint(tree)
Output:
{'season': {1: {'weathersit': {1.0: {'workingday': {0.0: 2407.5666666666666, 1.0: 3284.28}}, 2.0: 2331.74, 3.0: 473.5}}, 2: {'weathersit': {1.0: {'workingday': {0.0: 5850.178571428572, 1.0: 5340.06}}, 2.0: 4419.595744680851, 3.0: 1169.0}}, 3: {'weathersit': {1.0: {'holiday': {0.0: {'workingday': {0.0: 1.0: {1.0: 5996.090909090909, 2.0: 6093.058823529412, 3.0: 6043.6, 4.0: 6538.428571428572, 5.0: 6050.2307692307695}}}}, 1.0: 7403.0}}, 2.0: 5242.617647058823, 3.0: 2276.0}}, 4: {'weathersit': {1.0: {'holiday': {0.0: {'workingday': {0.0: 2727, 1.0: 4186}}, 1.0: 3101.25}}, 2.0: 4894.861111111111, 3.0: 1961.6}}}}
这就是我们最终的回归树模型。恭喜——大功告成!
As announced for the implementation of our regression tree model we will use the UCI bike sharing dataset
where we will use all 731 instances as well as a subset of the original 16 attributes. As attributes we use the
features: {'season', 'holiday', 'weekday', 'workingday', 'wheathersit', 'cnt'} where the {'cnt'} feature serves as
our target feature and represents the number of total rented bikes per day.
The first five rows of the dataset look as follows:
import pandas as pd
dataset = pd.read_csv("data/day.csv",usecols=['season','holida
y','weekday','workingday','weathersit','cnt'])
dataset.sample(frac=1).head()
Output:
season
holiday
weekday
workingday
weathersit
cnt
458
2
0
2
1
1
6772
245
3
0
6
0
1
4484
86
2
0
1
1
1
2028
333
4
0
3
1
1
3613
507
2
0
2
1
2
6073
We will now start adapting the originally created classification algorithm. For further comments to the code I
refer the reader to the previous chapter about Classification Trees.
"""
Make the imports of python packages needed
"""
import pandas as pd
import numpy as np
from pprint import pprint
import matplotlib.pyplot as plt
from matplotlib import style
423
style.use("fivethirtyeight")
#Import the dataset and define the feature and target columns#
dataset = pd.read_csv("data/day.csv",usecols=['season','holida
y','weekday','workingday','weathersit','cnt']).sample(frac=1)
mean_data = np.mean(dataset.iloc[:,-1])
##################################################################
#########################################
##################################################################
#########################################
"""
Calculate the varaince of a dataset
This function takes three arguments.
1. data = The dataset for whose feature the variance should be cal
culated
2. split_attribute_name = the name of the feature for which the we
ighted variance should be calculated
3. target_name = the name of the target feature. The default for t
his example is "cnt"
"""
def var(data,split_attribute_name,target_name="cnt"):
feature_values = np.unique(data[split_attribute_name])
feature_variance = 0
for value in feature_values:
#Create the data subsets --> Split the original data alon
g the values of the split_attribute_name feature
# and reset the index to not run into an error while usin
g the df.loc[] operation below
subset = data.query('{0}=={1}'.format(split_attribute_nam
e,value)).reset_index()
#Calculate the weighted variance of each subse
t
value_var = (len(subset)/len(data))*np.var(subset[target_n
ame],ddof=1)
#Calculate the weighted variance of the feature
feature_variance+=value_var
return feature_variance
##################################################################
424
#########################################
##################################################################
#########################################
def Classification(data,originaldata,features,min_instances,targe
t_attribute_name,parent_node_class = None):
"""
Classification Algorithm: This function takes the same 5 param
eters as the original classification algorithm in the
previous chapter plus one parameter (min_instances) which defi
nes the number of minimal instances
per node as early stopping criterion.
"""
#Define the stopping criteria --> If one of this is satisfie
d, we want to return a leaf node#
#########This criterion is new########################
#If all target_values have the same value, return the mean val
ue of the target feature for this dataset
if len(data) <= int(min_instances):
return np.mean(data[target_attribute_name])
#######################################################
#If the dataset is empty, return the mean target feature valu
e in the original dataset
elif len(data)==0:
return np.mean(originaldata[target_attribute_name])
#If the feature space is empty, return the mean target featur
e value of the direct parent node --> Note that
#the direct parent node is that node which has called the curr
ent run of the algorithm and hence
#the mean target feature value is stored in the parent_node_cl
ass variable.
elif len(features) ==0:
return parent_node_class
#If none of the above holds true, grow the tree!
else:
#Set the default value for this node --> The mean target f
eature value of the current node
parent_node_class = np.mean(data[target_attribute_name])
#Select the feature which best splits the dataset
item_values = [var(data,feature) for feature in features]
425
#Return the variance for features in the dataset
best_feature_index = np.argmin(item_values)
best_feature = features[best_feature_index]
#Create the tree structure. The root gets the name of the
feature (best_feature) with the minimum variance.
tree = {best_feature:{}}
#Remove the feature with the lowest variance from the feat
ure space
features = [i for i in features if i != best_feature]
#Grow a branch under the root node for each possible valu
e of the root node feature
for value in np.unique(data[best_feature]):
value = value
#Split the dataset along the value of the feature wit
h the lowest variance and therewith create sub_datasets
sub_data = data.where(data[best_feature] == value).dro
pna()
#Call the Calssification algorithm for each of those s
ub_datasets with the new parameters --> Here the recursion comes i
n!
subtree = Classification(sub_data,originaldata,feature
s,min_instances,'cnt',parent_node_class = parent_node_class)
#Add the sub tree, grown from the sub_dataset to the t
ree under the root node
tree[best_feature][value] = subtree
return tree
##################################################################
#########################################
##################################################################
#########################################
"""
426
Predict query instances
"""
def predict(query,tree,default = mean_data):
for key in list(query.keys()):
if key in list(tree.keys()):
try:
result = tree[key][query[key]]
except:
return default
result = tree[key][query[key]]
if isinstance(result,dict):
return predict(query,result)
else:
return result
##################################################################
#########################################
##################################################################
#########################################
"""
Create a training as well as a testing set
"""
def train_test_split(dataset):
training_data = dataset.iloc[:int(0.7*len(dataset))].reset_ind
ex(drop=True)#We drop the index respectively relabel the index
#starting form 0, because we do not want to run into errors re
garding the row labels / indexes
testing_data = dataset.iloc[int(0.7*len(dataset)):].reset_inde
x(drop=True)
return training_data,testing_data
training_data = train_test_split(dataset)[0]
testing_data = train_test_split(dataset)[1]
##################################################################
#########################################
##################################################################
#########################################
"""
Compute the RMSE
"""
427
def test(data,tree):
#Create new query instances by simply removing the target feat
ure column from the original dataset and
#convert it to a dictionary
queries = data.iloc[:,:-1].to_dict(orient = "records")
#Create a empty DataFrame in whose columns the prediction of t
he tree are stored
predicted = []
#Calculate the RMSE
for i in range(len(data)):
predicted.append(predict(queries[i],tree,mean_data))
RMSE = np.sqrt(np.sum(((data.iloc[:,-1]-predicted)**2)/len(dat
a)))
return RMSE
##################################################################
#########################################
##################################################################
#########################################
"""
Train the tree, Print the tree and predict the accuracy
"""
tree = Classification(training_data,training_data,training_data.co
lumns[:-1],5,'cnt')
pprint(tree)
print('#'*50)
print('Root mean square error (RMSE): ',test(testing_data,tree))
428
{'season': {1: {'weathersit': {1.0: {'workingday': {0.0: {'holiday': {0.0:
{0.0: 2398.1071428571427,
6.0: 2398.1071428571427}},
{1.0: 3284.28,
1.0:
1.0: {'holiday': {0.0:
2.0: 3284.28,
3.0: 3284.28,
4.0: 3284.28,
5.0: 3284.28}}}}}},
2.0: {'holiday': {0.0: {'weekday': {0.0: 258
1.0: 218
65,
2.0: {'w
{1.0: 2140.6666666666665}},
3.0: {'w
{1.0: 2049.0}},
4.0: {'w
{1.0: 3105.714285714286}},
5.0: {'w
{1.0: 2844.5454545454545}},
6.0: {'w
{0.0: 1757.111111111111}}}},
1.0: 1040.0}},
3.0: 473.5}},
2: {'weathersit': {1.0: {'workingday': {0.0: {'weekday': {0.0:
{0.0: 5728.2}},
1.0:
6667,
5.0:
6.0:
{0.0: 6206.142857142857}}}},
1.0: {'holiday': {0.0:
{1.0: 5340.06,
2.0:3.0:4.0:5340.06,
5340.06,
5340.06,
429
5.0: 5340.06}}}}}},
2.0: {'holiday': {0.0: {'workingday': {0.0:
{0.0: 4737.0,
6.0: 4349.7692307692305}},
1.0:
{1.0: 4446.294117647059,
2.0: 4446.294117647059,
3.0: 4446.294117647059,
4.0: 4446.294117647059,
5.0: 5975.333333333333}}}}}},
3.0: 1169.0}},
3: {'weathersit': {1.0: {'holiday': {0.0: {'workingday': {0.0:
{0.0: 5715.0,
6.0: 5715.0}},
1.0:
{1.0: 6148.342857142857,
2.0: 6148.342857142857,
3.0: 6148.342857142857,
4.0: 6148.342857142857,
5.0: 6148.342857142857}}}},
1.0: 7403.0}},
2.0: {'workingday': {0.0: {'holiday': {0.0:
{0.0: 4537.5,
6.0: 5028.8}},
1.0:
1.0: {'holiday': {0.0:
{1.0: 6745.25,
2.0: 5222.4,
3.0: 5554.0,
4.0: 4580.0,
5.0: 5389.409090909091}}}}}},
430
3.0: 2276.0}},
4: {'weathersit': {1.0: {'holiday': {0.0: {'workingday': {0.0:
{0.0: 4974.772727272727,
6.0: 4974.772727272727}},
{1.0: 5174.906976744186,
1.0:
2.0: 5174.906976744186,
3.0: 5174.906976744186,
4.0: 5174.906976744186,
5.0: 5174.906976744186}}}},
1.0: 3101.25}},
2.0: {'weekday': {0.0: 3795.6666666666665,
1.0: 4536.0,
2.0: {'holiday': {0.0: {'w
{1.0: 4440.875}}}},
3.0: 5446.4,
4.0: 5888.4,
5.0: 5773.6,
6.0: 4215.8}},
3.0: {'weekday': {1.0: 1393.5,
2.0: 2946.6666666666665,
3.0: 1840.5,
6.0: 627.0}}}}}}
##################################################
Root mean square error (RMSE): 1623.9891244058906
Above we can see RMSE for a minimum number of 5 instances per node. But for the time being, we have no
idea how bad or good that is. To get a feeling about the "accuracy" of our model we can plot kind of a learning
curve where we plot the number of minimal instances against the RMSE.
"""
Plot the RMSE with respect to the minimum number of instances
"""
fig = plt.figure()
ax0 = fig.add_subplot(111)
RMSE_test = []
RMSE_train = []
for i in range(1,100):
tree = Classification(training_data,training_data,training_dat
431
a.columns[:-1],i,'cnt')
RMSE_test.append(test(testing_data,tree))
RMSE_train.append(test(training_data,tree))
ax0.plot(range(1,100),RMSE_test,label='Test_Data')
ax0.plot(range(1,100),RMSE_train,label='Train_Data')
ax0.legend()
ax0.set_title('RMSE with respect to the minumim number of instance
s per node')
ax0.set_xlabel('#Instances')
ax0.set_ylabel('RMSE')
plt.show()
As we can see, increasing the minimum number of instances per node leads to a lower RMSE of our test data
until we reach approximately the number of 50 instances per node. Here the Test_Data curve kind of flattens
out and an additional increase in the minimum number of instances per leaf does not dramatically decrease the
RMSE of our testing set.
Lets plot the tree with a minimum instance number of 50.
tree = Classification(training_data,training_data,training_data.co
lumns[:-1],50,'cnt')
pprint(tree)
432
{'season': {1: {'weathersit': {1.0: {'workingday': {0.0: 2407.5666666666666
1.0: 3284.28}},
2.0: 2331.74,
3.0: 473.5}},
2: {'weathersit': {1.0: {'workingday': {0.0: 5850.178571428572,
1.0: 5340.06}},
2.0: 4419.595744680851,
3.0: 1169.0}},
3: {'weathersit': {1.0: {'holiday': {0.0: {'workingday': {0.0:
1.0:
{1.0: 5996.090909090909,
2.0: 6093.058823529412,
3.0: 6043.6,
4.0: 6538.428571428572,
5.0: 6050.2307692307695}}}},
1.0: 7403.0}},
2.0: 5242.617647058823,
3.0: 2276.0}},
4: {'weathersit': {1.0: {'holiday': {0.0: {'workingday': {0.0:
2727,
1.0:
4186}},
1.0: 3101.25}},
2.0: 4894.861111111111,
3.0: 1961.6}}}}
So thats our final regression tree model. Congratulations - Done! -
使用 Scikit-learn 的回归树模型 (Using Scikit-learn's Regression Tree Model)
既然我们已经从零开始构建了一个回归树模型,接下来我们将使用 Scikit-learn (sklearn) 预打包的回归树模型
sklearn.tree.DecisionTreeRegressor
。整个过程遵循 Scikit-learn 的通用 API,步骤一如既往:-
导入模型
-
模型参数化
-
数据预处理,创建描述性特征集和目标特征集
-
训练模型
-
预测新的查询实例
为了方便起见,我们将继续使用之前创建的训练和测试数据。
Python# 导入回归树模型 from sklearn.tree import DecisionTreeRegressor # 模型参数化 # 我们将使用均方误差(即方差)作为分裂标准,并将每个叶节点的最小实例数设置为 5 regression_model = DecisionTreeRegressor(criterion="mse", min_samples_leaf=5) # 训练模型 # training_data.iloc[:,:-1] 选择所有行和除最后一列外的所有列作为特征 # training_data.iloc[:,-1:] 选择所有行和最后一列作为目标 regression_model.fit(training_data.iloc[:,:-1], training_data.iloc[:,-1:]) # 预测未见的查询实例 predicted = regression_model.predict(testing_data.iloc[:,:-1]) # 计算并绘制 RMSE RMSE = np.sqrt(np.sum(((testing_data.iloc[:,-1]-predicted)**2)/len(testing_data.iloc[:,-1]))) print(RMSE)
Output:
1592.7501629176463
将每个叶节点的最小实例数设置为 5 时,我们得到的 RMSE 与上面我们自己构建的模型几乎相同。此外,对于这个模型,我们也将绘制 RMSE 随每个叶节点最小实例数的变化曲线,以评估哪个最小实例数参数能产生最小的 RMSE。
绘制 RMSE 随最小实例数的变化曲线 (Plotting RMSE with Respect to Minimum Instances)
Python""" 绘制 RMSE 随最小实例数的变化曲线 """ fig = plt.figure() ax0 = fig.add_subplot(111) RMSE_train = [] RMSE_test = [] for i in range(1,100): # 参数化模型,并让 i 作为每个叶节点的最小实例数 regression_model = DecisionTreeRegressor(criterion="mse", min_samples_leaf=i) # 训练模型 regression_model.fit(training_data.iloc[:,:-1], training_data.iloc[:,-1:]) # 预测查询实例 predicted_train = regression_model.predict(training_data.iloc[:,:-1]) predicted_test = regression_model.predict(testing_data.iloc[:,:-1]) # 计算并添加 RMSE 值 RMSE_train.append(np.sqrt(np.sum(((training_data.iloc[:,-1]-predicted_train)**2)/len(training_data.iloc[:,-1])))) RMSE_test.append(np.sqrt(np.sum(((testing_data.iloc[:,-1]-predicted_test)**2)/len(testing_data.iloc[:,-1])))) ax0.plot(range(1,100), RMSE_test, label='Test_Data') ax0.plot(range(1,100), RMSE_train, label='Train_Data') ax0.legend() ax0.set_title('RMSE with respect to the minimum number of instances per node') ax0.set_xlabel('#Instances') ax0.set_ylabel('RMSE') plt.show()
结果分析 (Results Analysis)
使用 Scikit-learn 预打包的回归树模型,当每个节点大约有 10 个实例时,RMSE 达到最小值。尽管如此,相对于实例数量的最小 RMSE 值与我们自己创建的模型计算出的值近似相同。此外,Scikit-learn 决策树模型的 RMSE 在每个节点的实例数量较大时也会趋于平稳。
参考文献 (References)
-
http://nbviewer.jupyter.org/gist/jwdink/9715a1a30e8c7f50a572
-
John D. Kelleher, Brian Mac Namee, Aoife D'Arcy, 2015. Machine Learning for Predictive Data Analytics. Cambridge, Massachusetts: The MIT Press.
-
Lior Rokach, Oded Maimon, 2015. Data Mining with Decision Trees. 2nd Ed. Ben-Gurion, Israel, Tel-Aviv, Israel: World Scientific.
-
Tom M. Mitchel, 1997. Machine Learning. New York, NY, USA: McGraw-Hill.
您对 Scikit-learn 的
DecisionTreeRegressor
模型与我们自己实现的模型在性能上的相似性有什么看法?这是否符合您的预期?
Since we have now build a Regression Tree model from scratch we will use sklearn's prepackaged Regression
Tree model sklearn.tree.DecisionTreeRegressor. The procedure follows the general sklearn API and is as
always:
1.
2.
3.
4.
5.
Import the model
Parametrize the model
Preprocess the data and create a descriptive feature set as well as a target feature set
Train the model
Predict new query instances
For convenience we will use the training and testing data from above.
#Import the regression tree model
from sklearn.tree import DecisionTreeRegressor
#Parametrize the model
#We will use the mean squered error == varince as spliting criteri
a and set the minimum number
#of instances per leaf = 5
regression_model = DecisionTreeRegressor(criterion="mse",min_sampl
es_leaf=5)
#Fit the model
regression_model.fit(training_data.iloc[:,:-1],training_data.ilo
c[:,-1:])
#Predict unseen query instances
predicted = regression_model.predict(testing_data.iloc[:,:-1])
#Compute and plot the RMSE
RMSE = np.sqrt(np.sum(((testing_data.iloc[:,-1]-predicted)**2)/le
n(testing_data.iloc[:,-1])))
RMSE
Output:1592.7501629176463
With a parameterized minimum number of 5 instances per leaf node, we get nearly the same RMSE as with
434
our own built model above. Also for this model we will plot the RMSE against the minimum number of
instances per leaf node to evaluate the minimum number of instances parameter which yields the minimum
RMSE.
"""
Plot the RMSE with respect to the minimum number of instances
"""
fig = plt.figure()
ax0 = fig.add_subplot(111)
RMSE_train = []
RMSE_test = []
for i in range(1,100):
#Paramterize the model and let i be the number of minimum inst
ances per leaf node
regression_model = DecisionTreeRegressor(criterion="mse",min_s
amples_leaf=i)
#Train the model
regression_model.fit(training_data.iloc[:,:-1],training_data.i
loc[:,-1:])
#Predict query instances
predicted_train = regression_model.predict(training_data.ilo
c[:,:-1])
predicted_test = regression_model.predict(testing_data.ilo
c[:,:-1])
#Calculate and append the RMSEs
RMSE_train.append(np.sqrt(np.sum(((training_data.iloc[:,-1]-pr
edicted_train)**2)/len(training_data.iloc[:,-1]))))
RMSE_test.append(np.sqrt(np.sum(((testing_data.iloc[:,-1]-pred
icted_test)**2)/len(testing_data.iloc[:,-1]))))
ax0.plot(range(1,100),RMSE_test,label='Test_Data')
ax0.plot(range(1,100),RMSE_train,label='Train_Data')
ax0.legend()
ax0.set_title('RMSE with respect to the minumim number of instance
s per node')
ax0.set_xlabel('#Instances')
ax0.set_ylabel('RMSE')
plt.show()
435
Using sklearns prepackaged regression tree model yields a minimum RMSE with ≈ 10 instances per node.
Though, the values for the minimum RMSE with respect to the number of instances are ≈ the same as
computed with our own created model. Additionally, the RMSE of sklearns decision tree model also flattens
out for large numbers of instances per node.
References:
•••••https://www.analyticsvidhya.com/blog/2016/04/complete-tutorial-tree-based-modeling-scratch-
in-python/
http://nbviewer.jupyter.org/gist/jwdink/9715a1a30e8c7f50a572
John D. Kelleher, Brian Mac Namee, Aoife D'Arcy, 2015. Machine Learning for Predictiive
Data Analytics. Cambridge, Massachusetts: The MIT Press.
Lior Rokach, Oded Maimon, 2015. Data Mining with Decision Trees. 2nd Ed. Ben-Gurion,
Israel, Tel-Aviv, Israel: Wolrd Scientific.
Tom M. Mitchel, 1997. Machine Learning. New York, NY, USA: McGraw-Hill. -
-
TensorFlow 简介
TensorFlow 是一个用于处理各种机器学习任务的开源软件库。它既是一个符号数学库,也被用作构建和训练神经网络的系统,以检测和破译模式与关联,这类似于人类的学习和推理过程。它被 Google 用于研究和生产,并经常取代其闭源的前身 DistBelief。TensorFlow 最初由 Google Brain 团队为 Google 内部使用而开发,并于 2015 年 11 月 9 日在 Apache 2.0 开源许可证下发布。
TensorFlow 提供了 Python API,以及 C++、Haskell、Java、Go 和 Rust API。
张量 (Tensors)
张量可以表示为多维数值数组。一个张量有其秩 (rank) 和形状 (shape),其中秩是它的维度数量,形状是每个维度的大小。
-
秩为 0 的张量,即形状为 () 的标量:
42
-
秩为 1 的张量,即形状为 (3,) 的向量:
[1, 2, 3]
-
秩为 2 的张量,即形状为 (2, 3) 的矩阵:
[[1, 2, 3], [3, 2, 1]]
-
秩为 3 的张量,形状为 (2, 2, 2):
[ [[3, 4], [1, 2]], [[3, 5], [8, 9]]]
Output:
[[[3, 4], [1, 2]], [[3, 5], [8, 9]]]
在 TensorFlow 中,所有数据都以张量的形式表示。它是唯一的数据结构类型,包括各种数据类型如:
tf.float32, tf.float64, tf.int8, tf.int16, ..., tf.int64, tf.uint8, ...
TensorFlow 程序的结构
TensorFlow 程序由两个独立的部分组成:
-
构建阶段:在此阶段创建计算图 (Computational Graph)。
-
执行阶段:在此阶段运行计算图,这通常在会话 (Session) 中完成。
示例:构建和运行计算图
Pythonimport tensorflow as tf # --- 计算图的构建阶段 --- # 定义常量张量 c1 = tf.constant(0.034) c2 = tf.constant(1000.0) # 定义操作节点 x = tf.multiply(c1, c1) # 乘法操作 y = tf.multiply(c1, c2) # 乘法操作 final_node = tf.add(x, y) # 加法操作,作为最终节点 # --- 会话的运行阶段 --- # 使用 with 语句运行会话,确保会话结束后资源被正确释放 with tf.Session() as sess: # 运行最终节点,触发整个计算图的评估 result = sess.run(final_node) print(result, type(result))
Output:
34.0012 <class 'numpy.float32'>
您还可以指定张量的数据类型,例如
tf.float64
以获得更高的精度:Pythonimport tensorflow as tf # --- 计算图的构建阶段 --- # 定义常量张量并指定数据类型为 float64 c1 = tf.constant(0.034, dtype=tf.float64) c2 = tf.constant(1000.0, dtype=tf.float64) # 定义操作节点 x = tf.multiply(c1, c1) y = tf.multiply(c1, c2) final_node = tf.add(x, y) # --- 会话的运行阶段 --- with tf.Session() as sess: result = sess.run(final_node) print(result, type(result))
Output:
34.001156 <class 'numpy.float64'>
张量也可以是向量或更高维的数组:
Pythonimport tensorflow as tf # --- 计算图的构建阶段 --- # 定义常量张量为向量,并指定数据类型为 float64 c1 = tf.constant([3.4, 9.1, -1.2, 9], dtype=tf.float64) c2 = tf.constant([3.4, 9.1, -1.2, 9], dtype=tf.float64) # 定义操作节点 x = tf.multiply(c1, c1) y = tf.multiply(c1, c2) final_node = tf.add(x, y) # --- 会话的运行阶段 --- with tf.Session() as sess: result = sess.run(final_node) print(result, type(result))
Output:
[ 23.12 165.62 2.88 162. ] <class 'numpy.ndarray'>
计算图的本质
一个计算图是由一系列 TensorFlow 操作组成的节点图。让我们构建一个简单的计算图。每个节点接受零个或多个张量作为输入,并产生一个张量作为输出。常量节点不接受任何输入。
请注意,仅仅打印节点本身并不会输出数值。我们只是定义了一个计算图,但还没有进行任何数值计算!
Pythonc1 = tf.constant([3.4, 9.1, -1.2, 9], dtype=tf.float64) c2 = tf.constant([3.4, 9.1, -1.2, 9], dtype=tf.float64) x = tf.multiply(c1, c1) y = tf.multiply(c1, c2) final_node = tf.add(x, y) print(c1) print(x) print(final_node)
Output:
Tensor("Const_6:0", shape=(4,), dtype=float64) Tensor("Mul_6:0", shape=(4,), dtype=float64) Tensor("Add_3:0", shape=(4,), dtype=float64)
要评估这些节点,我们必须在会话中运行计算图。会话封装了 TensorFlow 运行时的控制和状态。以下代码创建一个
Session
对象,然后调用其run
方法来运行计算图的足够部分以评估final_node
。首先,我们创建一个会话对象:
Pythonsession = tf.Session()
现在,我们可以通过启动会话对象的
run
方法来评估计算图:Pythonresult = session.run(final_node) print(result) print(type(result))
Output:
[ 23.12 165.62 2.88 162. ] <class 'numpy.ndarray'>
当然,当我们完成时,我们需要关闭会话:
Pythonsession.close()
然而,通常使用
with
语句是更好的做法,正如我们在入门示例中所示,因为它可以确保会话在代码块结束时自动关闭,避免资源泄露。
与 NumPy 的相似性
我们将使用 NumPy 重写以下 TensorFlow 程序,以展示它们在操作上的相似之处。
TensorFlow 版本:
Pythonimport tensorflow as tf session = tf.Session() x = tf.range(12) # 创建一个从0到11的张量 print(session.run(x)) # 运行并打印张量 x 的值 x2 = tf.reshape(tensor=x, # 将 x 重新塑形为 3x4 的矩阵 shape=(3, 4)) x2 = tf.reduce_sum(x2, reduction_indices=[0]) # 对矩阵按列求和 (reduction_indices=[0] 表示对第0维求和) res = session.run(x2) # 运行并打印 x2 的值 print(res) x3 = tf.eye(5, 5) # 创建一个 5x5 的单位矩阵 res = session.run(x3) # 运行并打印 x3 的值 print(res)
Output:
[ 0 1 2 3 4 5 6 7 8 9 10 11] [12 15 18 21] [[ 1. 0. 0. 0. 0.] [ 0. 1. 0. 0. 0.] [ 0. 0. 1. 0. 0.] [ 0. 0. 0. 1. 0.] [ 0. 0. 0. 0. 1.]]
NumPy 版本 (功能类似):
Pythonimport numpy as np x = np.arange(12) # 创建一个从0到11的 NumPy 数组 print(x) x2 = x.reshape((3, 4)) # 将 x 重新塑形为 3x4 的矩阵 res = x2.sum(axis=0) # 对矩阵按列求和 (axis=0 表示对第0轴求和) print(res) x3 = np.eye(5, 5) # 创建一个 5x5 的单位矩阵 print(x3)
Output:
[ 0 1 2 3 4 5 6 7 8 9 10 11] [12 15 18 21] [[ 1. 0. 0. 0. 0.] [ 0. 1. 0. 0. 0.] [ 0. 0. 1. 0. 0.] [ 0. 0. 0. 1. 0.] [ 0. 0. 0. 0. 1.]]
TensorBoard
TensorFlow 提供了借助名为 TensorBoard 的可视化工具来调试和优化程序的功能。
-
TensorFlow 在执行过程中会创建必要的数据。
-
这些数据存储在跟踪文件 (trace files) 中。
-
TensorBoard 可以通过浏览器访问
http://localhost:6006/
来查看。
我们可以运行以下示例程序,它将创建一个名为 "output" 的目录。然后,我们可以运行
tensorboard --logdir output
命令,它将启动一个 Web 服务器,并显示类似TensorBoard 0.1.8 at http://marvin:6006 (Press CTRL+C to quit)
的信息。Pythonimport tensorflow as tf p = tf.constant(0.034) c = tf.constant(1000.0) x = tf.add(c, tf.multiply(p, c)) # x = c + p*c x = tf.add(x, tf.multiply(p, x)) # x = x + p*x with tf.Session() as sess: # 创建 FileWriter 对象,将计算图写入 "output" 目录 writer = tf.summary.FileWriter("output", sess.graph) print(sess.run(x)) # 运行并打印 x 的值 writer.close() # 关闭 FileWriter
Output:
1069.16
运行此代码后,您可以在 TensorBoard 中查看包含计算图的可视化结果。
占位符 (Placeholders)
计算图可以被参数化以接受外部输入,这些输入被称为占位符。占位符的值在会话运行图时提供。
Pythonimport tensorflow as tf # 定义两个占位符,它们的数据类型是 float32 c1 = tf.placeholder(tf.float32) c2 = tf.placeholder(tf.float32) # 定义使用占位符的计算操作 x = tf.multiply(c1, c1) y = tf.multiply(c1, c2) final_node = tf.add(x, y) with tf.Session() as sess: # 使用 .eval() 方法运行 final_node,并通过字典提供 c1 和 c2 的值 result = final_node.eval({c1: 3.8, c2: 47.11}) print(result) # 占位符也可以接受数组作为输入 result = final_node.eval({c1: [3, 5], c2: [1, 3]}) print(result)
Output:
193.458 [ 12. 40.]
另一个使用 NumPy 数组作为占位符输入的例子:
Pythonimport tensorflow as tf import numpy as np v1 = np.array([3, 4, 5]) v2 = np.array([4, 1, 1]) # 定义占位符,并指定形状为 (3,),表示一维数组,长度为 3 c1 = tf.placeholder(tf.float32, shape=(3,)) c2 = tf.placeholder(tf.float32, shape=(3,)) x = tf.multiply(c1, c1) y = tf.multiply(c1, c2) final_node = tf.add(x, y) with tf.Session() as sess: # 传入 NumPy 数组作为占位符的值 result = final_node.eval({c1: v1, c2: v2}) print(result)
Output:
[ 21. 20. 30.]
tf.placeholder
函数说明placeholder( dtype, shape=None, name=None )
这个函数用于插入一个张量的占位符,该张量将始终被馈送 (fed)。它返回一个
Tensor
对象,可以用作馈送值的句柄,但不能直接评估。重要提示:如果直接评估此张量,它将产生错误。它的值必须通过
feed_dict
可选参数提供给以下方法:-
Session.run()
-
Tensor.eval()
-
Operation.run()
参数 (Args):
-
dtype
: 要馈送的张量中元素的类型。 -
shape
: 要馈送的张量的形状(可选)。如果未指定形状,则可以馈送任何形状的张量。 -
name
: 操作的名称(可选)。
变量 (Variables)
变量用于向计算图添加可训练的参数。它们通过类型和初始值构建。当你调用
tf.Variable
时,变量并不会立即被初始化。要初始化 TensorFlow 图中的所有变量,我们必须调用tf.global_variables_initializer
:Pythonimport tensorflow as tf W = tf.Variable([.5], dtype=tf.float32) # 定义权重 W,初始值为 0.5 b = tf.Variable([-1], dtype=tf.float32) # 定义偏置 b,初始值为 -1 x = tf.placeholder(tf.float32) # 定义输入 x 的占位符 model = W * x + b # 定义线性模型:W * x + b with tf.Session() as sess: init = tf.global_variables_initializer() # 获取初始化所有变量的操作 sess.run(init) # 运行初始化操作,实际初始化 W 和 b print(sess.run(model, {x: [1, 2, 3, 4]})) # 运行模型并传入 x 的值
Output:
[-0.5 0. 0.5 1. ]
变量与占位符的区别 (Difference Between Variables and Placeholders)
tf.Variable
和tf.placeholder
的区别在于值被传入的时间。-
如果你使用
tf.Variable
,你必须在声明时提供一个初始值。 -
使用
tf.placeholder
,你不需要提供初始值。它的值可以在运行时通过Session.run()
中的feed_dict
参数指定。
占位符用于将外部数据馈送到 TensorFlow 计算中,即从图外部传入!
如果你正在训练一个学习算法,占位符用于馈入你的训练数据。这意味着训练数据不是计算图的一部分。占位符的行为类似于 Python 的
input
语句。另一方面,TensorFlow 变量的行为或多或少像一个 Python 变量!示例:计算损失 (Loss)
Pythonimport tensorflow as tf W = tf.Variable([.5], dtype=tf.float32) b = tf.Variable([-1], dtype=tf.float32) x = tf.placeholder(tf.float32) # 输入特征的占位符 y = tf.placeholder(tf.float32) # 真实标签的占位符 model = W * x + b # 定义模型预测值 deltas = tf.square(model - y) # 计算预测值与真实值之间的平方差 (误差) loss = tf.reduce_sum(deltas) # 计算所有误差的平方和,作为总损失 with tf.Session() as sess: init = tf.global_variables_initializer() # 初始化所有变量 sess.run(init) # 运行初始化操作 # 运行损失计算,并传入 x 和 y 的具体值 print(sess.run(loss, {x: [1, 2, 3, 4], y: [1, 1, 1, 1]}))
Output:
3.5
重新赋值给变量 (Reassigning Values to Variables)
TensorFlow 变量的值可以在会话中通过
tf.assign()
操作重新赋值。Pythonimport tensorflow as tf W = tf.Variable([.5], dtype=tf.float32) b = tf.Variable([-1], dtype=tf.float32) x = tf.placeholder(tf.float32) y = tf.placeholder(tf.float32) model = W * x + b deltas = tf.square(model - y) loss = tf.reduce_sum(deltas) with tf.Session() as sess: init = tf.global_variables_initializer() sess.run(init) print(sess.run(loss, {x: [1, 2, 3, 4], y: [1, 1, 1, 1]})) W_a = tf.assign(W, [0.]) # 创建一个将 W 赋值为 0.0 的操作 b_a = tf.assign(b, [1.]) # 创建一个将 b 赋值为 1.0 的操作 sess.run( W_a ) # 运行 W 的赋值操作 sess.run( b_a) # 运行 b 的赋值操作 # sess.run( [W_a, b_a] ) # 或者也可以在一次 'run' 调用中同时运行多个操作 print(sess.run(loss, {x: [1, 2, 3, 4], y: [1, 1, 1, 1]})) # 再次计算损失
Output:
3.5 0.0
可以看到,在重新赋值
W
和b
后,模型的损失从3.5
变为0.0
,这说明新的W=0
和b=1
使模型0*x+1 = 1
完美匹配了目标y=[1,1,1,1]
。
使用梯度下降优化器 (GradientDescentOptimizer)
以下示例展示了如何使用
tf.train.GradientDescentOptimizer
来训练模型,使其最小化损失函数。Pythonimport tensorflow as tf W = tf.Variable([.5], dtype=tf.float32) b = tf.Variable([-1], dtype=tf.float32) x = tf.placeholder(tf.float32) y = tf.placeholder(tf.float32) model = W * x + b deltas = tf.square(model - y) loss = tf.reduce_sum(deltas) # 定义梯度下降优化器,学习率为 0.01 optimizer = tf.train.GradientDescentOptimizer(0.01) # 定义训练步骤:最小化损失函数 train = optimizer.minimize(loss) with tf.Session() as sess: init = tf.global_variables_initializer() # 初始化所有变量 sess.run(init) # 运行初始化操作 # 循环训练 1000 次 for _ in range(1000): sess.run(train, # 运行训练步骤 {x: [1, 2, 3, 4], y: [1, 1, 1, 1]}) # 传入训练数据 # 创建 FileWriter 用于 TensorBoard 可视化 writer = tf.summary.FileWriter("optimizer", sess.graph) print(sess.run([W, b])) # 打印训练后的 W 和 b 的值 writer.close() # 关闭 FileWriter
Output:
[array([ 3.91378126e-06], dtype=float32), array([ 0.99998844], dtype=float32)]
经过 1000 次训练迭代,
W
的值非常接近 0,而b
的值非常接近 1,这与手动重新赋值时的理想值相符,表明梯度下降优化器有效地学习了模型参数。
创建数据集 (Creating Data Sets)
我们将为梯度下降优化器创建一个更大的分类示例数据集。这里我们将生成两类数据点:“bad ones” 和 “good ones”,并将其保存到文件中。
Pythonimport numpy as np import matplotlib.pyplot as plt # 循环创建训练集和测试集 for quantity, suffix in [(1000, "train"), (200, "test")]: # 生成“坏点”:均值为 [-2, -2],协方差为 [[1, 0], [0, 1]] 的多元正态分布 samples = np.random.multivariate_normal([-2, -2], [[1, 0], [0, 1]], quantity) # 绘制“坏点” plt.plot(samples[:, 0], samples[:, 1], '.', label="bad ones " + suffix) # 给“坏点”添加标签 0 bad_ones = np.column_stack((np.zeros(quantity), samples)) # 生成“好点”:均值为 [1, 1],协方差为 [[1, 0.5], [0.5, 1]] 的多元正态分布 samples = np.random.multivariate_normal([1, 1], [[1, 0.5], [0.5, 1]], quantity) # 绘制“好点” plt.plot(samples[:, 0], samples[:, 1], '.', label="good ones " + suffix) # 给“好点”添加标签 1 good_ones = np.column_stack((np.ones(quantity), samples)) # 将“坏点”和“好点”堆叠起来,形成完整的数据集 sample = np.row_stack((bad_ones, good_ones)) # 将数据集保存到文本文件 np.savetxt("data/the_good_and_the_bad_ones_" + suffix + ".txt", sample, fmt="%1d %4.2f %4.2f") plt.legend() # 显示图例 plt.show() # 显示图表
TensorFlow 模型的训练与评估
这是一个使用 TensorFlow 构建和训练一个简单分类模型的完整示例。模型将学习区分“好点”和“坏点”。
Pythonimport os # 抑制 TensorFlow 警告信息,设置日志级别为 ERROR os.environ['TF_CPP_MIN_LOG_LEVEL']='2' import numpy as np import tensorflow as tf from matplotlib import pyplot as plt # --- 超参数和配置 --- number_of_samples_per_training_step = 100 # 每个训练步骤(批次)的样本数 num_of_epochs = 1 # 训练的总轮数(此处为 1 轮) num_labels = 2 # 类别数量 (0 和 1) # --- 辅助函数定义 --- # 评估函数:使用模型对输入 X 进行预测 def evaluation_func(X): # predicted_class 是一个 TensorFlow 操作,需要通过 sess.run 或 .eval() 运行 return predicted_class.eval(feed_dict={x:X}) # 绘制决策边界函数 def plot_boundary(X, Y, pred_func): # 确定绘图画布的边界 mins = np.amin(X, 0) # 获取每列的最小值 mins = mins - 0.1*np.abs(mins) # 稍微扩展边界 maxs = np.amax(X, 0) # 获取每列的最大值 maxs = maxs + 0.1*maxs # 稍微扩展边界 # 创建网格点,用于在整个特征空间上评估模型 xs, ys = np.meshgrid(np.linspace(mins[0], maxs[0], 300), np.linspace(mins[1], maxs[1], 300)) # 使用密集的网格评估模型 # np.c_ 将扁平化的 xs 和 ys 组合成 (N, 2) 形状的数组,作为模型的输入 Z = pred_func(np.c_[xs.flatten(), ys.flatten()]) # 将模型输出的预测结果 Z 重新塑形回网格的形状 Z = Z.reshape(xs.shape) # 绘制等高线图和训练样本 # contourf 填充等高线区域,颜色对应不同的类别 plt.contourf(xs, ys, Z, colors=('c', 'g', 'y', 'b')) # 绘制真实数据点 Xn = X[Y[:,1]==1] # 提取标签为 1 的数据点 plt.plot(Xn[:, 0], Xn[:, 1], "bo", label="Good Ones") # 绘制蓝色圆点 Xn = X[Y[:,1]==0] # 提取标签为 0 的数据点 plt.plot(Xn[:, 0], Xn[:, 1], "go", label="Bad Ones") # 绘制绿色圆点 plt.legend() # 显示图例 plt.show() # 显示图表 # 加载数据函数 def get_data(fname): data = np.loadtxt(fname) # 从文件加载数据 labels = data[:, :1] # 提取第一列作为标签 (形状如 [[0.], [0.], [1.], ...]) # 将标签转换为独热编码 (one-hot encoding) 格式 labels_one_hot = (np.arange(num_labels) == labels).astype(np.float32) data = data[:, 1:].astype(np.float32) # 提取除了第一列的特征数据 return data, labels_one_hot # --- 数据加载 --- data_train = "data/the_good_and_the_bad_ones_train.txt" data_test = "data/the_good_and_the_bad_ones_test.txt" train_data, train_labels = get_data(data_train) # 加载训练数据 test_data, test_labels = get_data(data_test) # 加载测试数据 train_size, num_features = train_data.shape # 获取训练样本数和特征数 # --- TensorFlow 图的构建 --- # 定义输入特征的占位符 (x) 和真实标签的占位符 (y_) x = tf.placeholder("float", shape=[None, num_features]) # None 表示批次大小不固定 y_ = tf.placeholder("float", shape=[None, num_labels]) # 独热编码的标签 # 定义模型的权重 (Weights) 和偏置 (b) 变量,并初始化为零 Weights = tf.Variable(tf.zeros([num_features, num_labels])) b = tf.Variable(tf.zeros([num_labels])) # 定义模型的输出:使用 softmax 激活函数的逻辑回归模型 # tf.matmul(x, Weights) + b 实现线性变换 # tf.nn.softmax 将输出转换为概率分布 y = tf.nn.softmax(tf.matmul(x, Weights) + b) # --- 优化器和损失函数 --- # 定义交叉熵损失函数 cross_entropy = -tf.reduce_sum(y_*tf.log(y)) # 定义训练步骤:使用梯度下降优化器最小化交叉熵损失,学习率为 0.01 train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy) # 为测试数据创建常量节点,以便在评估时不传入 feed_dict test_data_node = tf.constant(test_data) # --- 评估指标 --- # 预测类别:tf.argmax(y, 1) 获取每个样本预测概率最大的类别索引 predicted_class = tf.argmax(y, 1) # 判断预测是否正确:tf.equal 比较预测类别和真实类别 correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1)) # 计算准确率:将布尔值转换为浮点数并求平均 accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float")) # --- 会话运行和模型训练 --- with tf.Session() as sess: # 运行所有初始化器,准备可训练参数 (W 和 b) init = tf.global_variables_initializer() sess.run(init) # 迭代并训练模型 # 训练步数 = 总轮数 * (训练集大小 // 每个训练步的样本数) for step in range(num_of_epochs * train_size // number_of_samples_per_training_step): # 计算当前批次的起始偏移量 offset = (step * number_of_samples_per_training_step) % train_size # 获取当前批次的数据和标签 batch_data = train_data[offset:(offset + number_of_samples_per_training_step), :] batch_labels = train_labels[offset:(offset + number_of_samples_per_training_step)] # 将数据馈入模型并运行训练步骤 train_step.run(feed_dict={x: batch_data, y_: batch_labels}) # --- 训练结果输出 --- print('\n偏置向量 (Bias vector): ', sess.run(b)) print('权重矩阵 (Weight matrix):\n', sess.run(Weights)) print("\n应用于第一个数据集:") first = test_data[:1] # 获取测试数据的第一个样本 print(first) print("\nWx + b: ", sess.run(tf.matmul(first, Weights) + b)) # 打印线性模型的输出 # softmax 函数是一种广义的 logistic 函数,它将 K 维的任意实数向量 z “压缩”成一个 K 维的实数向量 σ(z), # 其值范围在 [0, 1] 之间且和为 1。 print("softmax(Wx + b): ", sess.run(tf.nn.softmax(tf.matmul(first, Weights) + b))) # 打印 softmax 输出 print("测试数据准确率 (Accuracy on test data): ", accuracy.eval(feed_dict={x: test_data, y_: test_labels})) print("训练数据准确率 (Accuracy on training data): ", accuracy.eval(feed_dict={x: train_data, y_: train_labels})) # --- 对新数据进行分类预测 --- print("\n对一些值进行分类:") print(evaluation_func([[-3, 7.3], [-1,8], [0, 0], [1, 0.0], [-1, 0]])) # 绘制决策边界 plot_boundary(test_data, test_labels, evaluation_func)
Output:
偏置向量 (Bias vector): [-0.78089082 0.78089082] 权重矩阵 (Weight matrix): [[-0.80193734 0.8019374 ] [-0.831303 0.831303 ]] 应用于第一个数据集: [[-1.05999994 -1.55999994]] Wx + b: [[ 1.36599553 -1.36599553]] softmax(Wx + b): [[ 0.93888813 0.06111182]] 测试数据准确率 (Accuracy on test data): 0.97 训练数据准确率 (Accuracy on training data): 0.9725 对一些值进行分类: [1 1 1 1 0]
TensorFlow is an open-source software library for machine learning across a range of tasks. It is a symbolic
math library, and also used as a system for building and training neural networks to detect and decipher
patterns and correlations, analogous to human learning and reasoning. It is used for both research and
production at Google often replacing its closed-source predecessor, DistBelief. TensorFlow was developed by
the Google Brain team for internal Google use. It was released under the Apache 2.0 open source license on 9
November 2015.
TensorFlow provides a Python API as well as C++, Haskell, Java, Go and Rust APIs.
A tensor can be represented as a
multidimensional array of numbers. A
tensor has its rank and shape, rank is its
number of dimensions and shape is the
size of each dimension.
# a rank 0 tensor, i.e.
a scalar with shape ():
42
# a rank 1 tensor, i.e.
a vector with shape (3,):
[1, 2, 3]
# a rank 2 tensor, i.e. a matrix with shape (2, 3):
[[1, 2, 3], [3, 2, 1]]
# a rank 3 tensor with shape (2, 2, 2) :
[ [[3, 4], [1, 2]], [[3, 5], [8, 9]]]
#
Output:[[[3, 4], [1, 2]], [[3, 5], [8, 9]]]
All data of TensorFlow is represented as tensors. It is the sole data structure:
tf.float32, tf.float64, tf.int8, tf.int16, ..., tf.int64, tf.uint8, ...
437
STRUCTURE OF TENSORFLOW PROGRAMS
TensorFlow programs consist of two
discrete sections:
1.2.A graph is created in the
construction phase.
The computational graph is
run in the execution phase,
which is a session.
EXAMPLE
import tensorflow as tf
# Computational Graph:
c1 = tf.constant(0.034)
c2 = tf.constant(1000.0)
x = tf.multiply(c1, c1)
y = tf.multiply(c1, c2)
final_node = tf.add(x, y)
# Running the session:
with tf.Session() as sess:
result = sess.run(final_node)
print(result, type(result))
34.0012 <class 'numpy.float32'>
import tensorflow as tf
# Computational Graph:
c1 = tf.constant(0.034, dtype=tf.float64)
c2 = tf.constant(1000.0, dtype=tf.float64)
x = tf.multiply(c1, c1)
y = tf.multiply(c1, c2)
final_node = tf.add(x, y)
# Running the session:
438
with tf.Session() as sess:
result = sess.run(final_node)
print(result, type(result))
34.001156 <class 'numpy.float64'>
import tensorflow as tf
# Computational Graph:
c1 = tf.constant([3.4, 9.1, -1.2, 9], dtype=tf.float64)
c2 = tf.constant([3.4, 9.1, -1.2, 9], dtype=tf.float64)
x = tf.multiply(c1, c1)
y = tf.multiply(c1, c2)
final_node = tf.add(x, y)
# Running the session:
with tf.Session() as sess:
result = sess.run(final_node)
print(result, type(result))
[
23.12
165.62
2.88
162.
] <class 'numpy.ndarray'>
A computational graph is a series of TensorFlow operations arranged into a graph of nodes. Let's build a
simple computational graph. Each node takes zero or more tensors as inputs and produces a tensor as an
output. Constant nodes take no input.
Printing the nodes does not output a numerical value. We have defined a computational graph but no
numerical evaluation has taken place!
c1 = tf.constant([3.4, 9.1, -1.2, 9], dtype=tf.float64)
c2 = tf.constant([3.4, 9.1, -1.2, 9], dtype=tf.float64)
x = tf.multiply(c1, c1)
y = tf.multiply(c1, c2)
final_node = tf.add(x, y)
print(c1)
print(x)
print(final_node)
Tensor("Const_6:0", shape=(4,), dtype=float64)
Tensor("Mul_6:0", shape=(4,), dtype=float64)
Tensor("Add_3:0", shape=(4,), dtype=float64)
439
To evaluate the nodes, we have to run the computational graph within a session. A session encapsulates the
control and state of the TensorFlow runtime. The following code creates a Session object and then invokes its
run method to run enough of the computational graph to evaluate node1 and node2. By running the
computational graph in a session as follows. We have to create a session object:
session = tf.Session()
Now, we can evaluate the computational graph by starting the run method of the session object:
result = session.run(final_node)
print(result)
print(type(result))
[ 23.12 165.62
2.88
162.
]
<class 'numpy.ndarray'>
Of course, we will have to close the session, when we are finished:
session.close()
It is usually a better idea to work with the with statement, as we did in the introductory examples!
SIMILARITY TO NUMPY
We will rewrite the following program with Numpy.
import tensorflow as tf
session = tf.Session()
x = tf.range(12)
print(session.run(x))
x2 = tf.reshape(tensor=x,
shape=(3, 4))
x2 = tf.reduce_sum(x2, reduction_indices=[0])
res = session.run(x2)
print(res)
x3 = tf.eye(5, 5)
res = session.run(x3)
print(res)
440
[ 0 1 2 3 4 5 6 7
8
9 10 11]
[12 15 18 21]
[[ 1. 0. 0. 0. 0.]
[ 0. 1. 0. 0. 0.]
[ 0. 0. 1. 0. 0.]
[ 0. 0. 0. 1. 0.]
[ 0. 0. 0. 0. 1.]]
Now a similar Numpy version:
import numpy as np
x = np.arange(12)
print(x)
x2 = x.reshape((3, 4))
res = x2.sum(axis=0)
print(res)
x3 = np.eye(5, 5)
print(x3)
[ 0 1 2 3 4 5 6 7
8
9 10 11]
[12 15 18 21]
[[ 1. 0. 0. 0. 0.]
[ 0. 1. 0. 0. 0.]
[ 0. 0. 1. 0. 0.]
[ 0. 0. 0. 1. 0.]
[ 0. 0. 0. 0. 1.]]
TENSORBOARD
••••TensorFlow provides functions to debug and optimize programs with the help of a visualization
tool called TensorBoard.
TensorFlow creates the necessary data during its execution.
The data are stored in trace files.
Tensorboard can be viewed from a browser using http://localhost:6006/
We can run the following example program, and it will create the directory "output" We can run now
tensorboard: tensorboard --logdir output
which will create a webserver: TensorBoard 0.1.8 at http://marvin:6006 (Press CTRL+C to quit)
import tensorflow as tf
p = tf.constant(0.034)
441
c = tf.constant(1000.0)
x = tf.add(c, tf.multiply(p, c))
x = tf.add(x, tf.multiply(p, x))
with tf.Session() as sess:
writer = tf.summary.FileWriter("output", sess.graph)
print(sess.run(x))
writer.close()
1069.16
The computational graph is included in the TensorBoard:
PLACEHOLDERS
A computational graph can be parameterized to accept external inputs, known as placeholders. The values for
placeholders are provided when the graph is run in a session.
442
import tensorflow as tf
c1 = tf.placeholder(tf.float32)
c2 = tf.placeholder(tf.float32)
x = tf.multiply(c1, c1)
y = tf.multiply(c1, c2)
final_node = tf.add(x, y)
with tf.Session() as sess:
result = final_node.eval( {c1: 3.8, c2: 47.11})
print(result)
result = final_node.eval( {c1: [3, 5], c2: [1, 3]})
print(result)
193.458
[ 12. 40.]
Another example:
import tensorflow as tf
import numpy as np
v1 = np.array([3, 4, 5])
v2 = np.array([4, 1, 1])
c1 = tf.placeholder(tf.float32, shape=(3,))
c2 = tf.placeholder(tf.float32, shape=(3,))
x = tf.multiply(c1, c1)
y = tf.multiply(c1, c2)
final_node = tf.add(x, y)
with tf.Session() as sess:
result = final_node.eval( {c1: v1, c2: v2})
print(result)
[ 21.
20.
30.]
placeholder( dtype, shape=None, name=None )
Inserts a placeholder for a tensor that will be always fed. It returns a Tensor that may be used as a handle for
feeding a value, but not evaluated directly.
Important: This tensor will produce an error if evaluated. Its value must be fed using the feed_dict optional
argument to
Session.run()
443
Tensor.eval()
Operation.run()
Args:
Parameter
Description
dtype:
The type of elements in the tensor to be fed.
shape:
The shape of the tensor to be fed (optional). If the shape is not specified, you can feed a tensor of any shape.
name:
A name for the operation (optional).
VARIABLES
Variables are used to add trainable parameters to a graph. They are constructed with a type and initial value.
Variables are not initialized when you call tf.Variable. To initialize the variables of a TensorFlow graph, we
have to call global_variables_initializer:
import tensorflow as tf
W = tf.Variable([.5], dtype=tf.float32)
b = tf.Variable([-1], dtype=tf.float32)
x = tf.placeholder(tf.float32)
model = W * x + b
with tf.Session() as sess:
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(model, {x: [1, 2, 3, 4]}))
[-0.5
0.
0.5
1. ]
DIFFERENCE BETWEEN VARIABLES AND PLACEHOLDERS
The difference between tf.Variable and tf.placeholder consists in the time when the values are passed. If you
use tf.Variable, you have to provide an initial value when you declare it. With tf.placeholder you don't have to
provide an initial value.
The value can be specified at run time with the feed_dict argument inside Session.run
A placeholder is used for feeding external data into a Tensorflow computation, i.e. from outside of the graph!
444
If you are training a learning algorithm, a placeholder is used for feeding in your training data. This means that
the training data is not part of the computational graph. The placeholder behaves similar to the Python "input"
statement. On the other hand a TensorFlow variable behaves more or less like a Python variable!
Example:
Calculating the loss:
import tensorflow as tf
W = tf.Variable([.5], dtype=tf.float32)
b = tf.Variable([-1], dtype=tf.float32)
x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)
model = W * x + b
deltas = tf.square(model - y)
loss = tf.reduce_sum(deltas)
with tf.Session() as sess:
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(loss, {x: [1, 2, 3, 4], y: [1, 1, 1, 1]}))
3.5
REASSIGNING VALUES TO VARIABLES
import tensorflow as tf
W = tf.Variable([.5], dtype=tf.float32)
b = tf.Variable([-1], dtype=tf.float32)
x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)
model = W * x + b
deltas = tf.square(model - y)
loss = tf.reduce_sum(deltas)
with tf.Session() as sess:
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(loss, {x: [1, 2, 3, 4], y: [1, 1, 1, 1]}))
445
W_a = tf.assign(W, [0.])
b_a = tf.assign(b, [1.])
sess.run( W_a )
sess.run( b_a)
# sess.run( [W_a, b_a] ) # alternatively in one 'run'
print(sess.run(loss, {x: [1, 2, 3, 4], y: [1, 1, 1, 1]}))
3.5
0.0
import tensorflow as tf
W = tf.Variable([.5], dtype=tf.float32)
b = tf.Variable([-1], dtype=tf.float32)
x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)
model = W * x + b
deltas = tf.square(model - y)
loss = tf.reduce_sum(deltas)
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)
with tf.Session() as sess:
init = tf.global_variables_initializer()
sess.run(init)
for _ in range(1000):
sess.run(train,
{x: [1, 2, 3, 4], y: [1, 1, 1, 1]})
writer = tf.summary.FileWriter("optimizer", sess.graph)
print(sess.run([W, b]))
writer.close()
[array([ 3.91378126e-06], dtype=float32), array([ 0.99998844], dt
ype=float32)]
CREATING DATA SETS
We will create data sets for a larger example for the GradientDescentOptimizer.
import numpy as np
import matplotlib.pyplot as plt
446
for quantity, suffix in [(1000, "train"), (200, "test")]:
samples = np.random.multivariate_normal([-2, -2], [[1, 0],
[0, 1]], quantity)
plt.plot(samples[:, 0], samples[:, 1], '.', label="bad ones "
+ suffix)
bad_ones = np.column_stack((np.zeros(quantity), samples))
samples = np.random.multivariate_normal([1, 1], [[1, 0.5],
[0.5, 1]], quantity)
plt.plot(samples[:, 0], samples[:, 1], '.', label="good ones
" + suffix)
good_ones = np.column_stack((np.ones(quantity), samples))
sample = np.row_stack((bad_ones, good_ones))
np.savetxt("data/the_good_and_the_bad_ones_" + suffix + ".tx
t", sample, fmt="%1d %4.2f %4.2f")
plt.legend()
plt.show()
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import numpy as np
import tensorflow as tf
from matplotlib import pyplot as plt
number_of_samples_per_training_step = 100
num_of_epochs = 1
447
num_labels = 2 # should be automatically determined
defevaluation_func(X):
return predicted_class.eval(feed_dict={x:X})
def plot_boundary(X, Y, pred_func):
# determine canvas borders
mins = np.amin(X, 0)
# array with column minimums
mins = mins - 0.1*np.abs(mins)
maxs = np.amax(X, 0)
# array with column maximums
maxs = maxs + 0.1*maxs
xs, ys = np.meshgrid(np.linspace(mins[0], maxs[0], 300),
np.linspace(mins[1], maxs[1], 300))
# evaluate model using the dense grid
# c_ creates one array with "points" from meshgrid:
Z = pred_func(np.c_[xs.flatten(), ys.flatten()])
# Z is one-dimensional and will be reshaped into 300 x 300:
Z = Z.reshape(xs.shape)
# Plot the contour and training examples
plt.contourf(xs, ys, Z, colors=('c', 'g', 'y', 'b'))
Xn = X[Y[:,1]==1]
plt.plot(Xn[:, 0], Xn[:, 1], "bo")
Xn = X[Y[:,1]==0]
plt.plot(Xn[:, 0], Xn[:, 1], "go")
plt.show()
def get_data(fname):
data = np.loadtxt(fname)
labels = data[:, :1] # array([[ 0.], [ 0.], [ 1.], ...]])
labels_one_hot = (np.arange(num_labels) == labels).astype(np.f
loat32)
data = data[:, 1:].astype(np.float32)
return data, labels_one_hot
data_train = "data/the_good_and_the_bad_ones_train.txt"
data_test = "data/the_good_and_the_bad_ones_test.txt"
train_data, train_labels = get_data(data_train)
test_data, test_labels = get_data(data_test)
448
train_size, num_features = train_data.shape
x = tf.placeholder("float", shape=[None, num_features])
y_ = tf.placeholder("float", shape=[None, num_labels])
Weights = tf.Variable(tf.zeros([num_features, num_labels]))
b = tf.Variable(tf.zeros([num_labels]))
y = tf.nn.softmax(tf.matmul(x, Weights) + b)
# Optimization.
cross_entropy = -tf.reduce_sum(y_*tf.log)
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cros
s_entropy)
# For the test data, hold the entire dataset in one constant node.
test_data_node = tf.constant(test_data)
# Evaluation.
predicted_class = tf.argmax(y, 1)
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
with tf.Session() as sess:
# Run all the initializers to prepare the trainable parameter
s.
init = tf.global_variables_initializer()
sess.run(init)
# Iterate and train.
for step in range(num_of_epochs * train_size // number_of_samp
les_per_training_step):
offset = (step * number_of_samples_per_training_step) % tr
ain_size
# get a batch of data
batch_data = train_data[offsetoffset +
number_of_samples_per_trai
ning_step), :]
batch_labels = train_labels[offsetoffset + number_of_sam
ples_per_training_step)]
449
s})
# feed data into the model
train_step.run(feed_dict={x: batch_data, y_: batch_label
print('\nBias vector: ', sess.run(b))
print('Weight matrix:\n', sess.run(Weights))
print("\nApplying model to first data set:")
first = test_data[:1]
print(first)
print("\nWx + b: ", sess.run(tf.matmul(first, Weights) + b))
# the softmax function, or normalized exponential function, i
s a generalization of the
# logistic function that "squashes" a K-dimensional vector z o
f arbitrary real values
# to a K-dimensional vector σ(z) of real values in the range
[0, 1] that add up to 1.
print("softmax(Wx + b): ", sess.run(tf.nn.softmax(tf.matmul(fi
rst, Weights) + b)))
print("Accuracy on test data: ", accuracy.eval(feed_dict={x: t
est_data, y_: test_labels}))
print("Accuracy on training data: ", accuracy.eval(feed_dic
t={x: train_data, y_: train_labels}))
# classify some values:
print(evaluation_func([[-3, 7.3], [-1,8], [0, 0], [1, 0.0],
[-1, 0]]))
plot_boundary(test_data, test_labels, evaluation_func)
450
Bias vector: [-0.78089082 0.78089082]
Weight matrix:
[[-0.80193734 0.8019374 ]
[-0.831303
0.831303 ]]
Applying model to first data set:
[[-1.05999994 -1.55999994]]
Wx + b: [[ 1.36599553 -1.36599553]]
softmax(Wx + b): [[ 0.93888813 0.06111182]]
Accuracy on test data: 0.97
Accuracy on training data: 0.9725
[1 1 1 1 0]
In [ ]:
In [ ]: -