为啥单层感知器在没有归一化的情况下收敛这么慢,即使边距很大?

Posted

技术标签:

【中文标题】为啥单层感知器在没有归一化的情况下收敛这么慢,即使边距很大?【英文标题】:Why does single-layer perceptron converge so slow without normalization, even when the margin is large?为什么单层感知器在没有归一化的情况下收敛这么慢,即使边距很大? 【发布时间】:2020-04-06 17:36:27 【问题描述】:

这个问题在我用别人写的一段代码(可以找到here)确认我的结果(可以找到Python Notebook here)后完全重写。这是我用来处理我的数据并计算收敛时间的代码:

import numpy as np
from matplotlib import pyplot as plt

class Perceptron(object):
    """Implements a perceptron network"""
    def __init__(self, input_size, lr=0.1, epochs=1000000):
        self.W = np.zeros(input_size+1)
        #self.W = np.random.randn(input_size+1)
        # add one for bias
        self.epochs = epochs
        self.lr = lr

    def predict(self, x):
        z = self.W.T.dot(x)
        return [1 if self.W.T.dot(x) >=0 else 0]

    def fit(self, X, d):
        errors = []
        for epoch in range(self.epochs):
            if (epoch + 1) % 10000 == 0: print('Epoch',epoch + 1)
            total_error = 0
            for i in range(d.shape[0]):
                x = np.insert(X[i], 0, 1)
                y = self.predict(x)
                e = d[i] - y
                total_error += np.abs(e)
                self.W = self.W + self.lr * e * x
                #print('W: ', self.W)
            errors += [total_error]
            if (total_error == 0):
                print('Done after', epoch, 'epochs')
                nPlot = 100
                plt.plot(list(range(len(errors)-nPlot, len(errors))), errors[-nPlot:])
                plt.show()
                break

if __name__ == '__main__':
    trainingSet = np.array([[279.25746446, 162.44072328,   1.        ],
                            [306.23240054, 128.3794866 ,   1.        ],
                            [216.67811217, 148.58167262,   1.        ],
                            [223.64431813, 197.75745016,   1.        ],
                            [486.68209275,  96.09115377,   1.        ],
                            [400.71323154, 125.18183395,   1.        ],
                            [288.87299305, 204.52217766,   1.        ],
                            [245.1492875 ,  55.75847006,  -1.        ],
                            [ 14.95991122, 185.92681911,   1.        ],
                            [393.92908798, 193.40527965,   1.        ],
                            [494.15988362, 179.23456285,   1.        ],
                            [235.59039363, 175.50868526,   1.        ],
                            [423.72071607,   9.50166894,  -1.        ],
                            [ 76.52735621, 208.33663341,   1.        ],
                            [495.1492875 ,  -7.73818431,  -1.        ]])
    X = trainingSet[:, :2]
    d = trainingSet[:, -1]
    d = np.where(d == -1, 1, 0)
    perceptron = Perceptron(input_size=2)
    perceptron.fit(X, d)
    print(perceptron.W)

训练集由 15 个点组成,具有较大的分离边距。 Perceptron 算法找到一个分隔符,如下所示,但经过多达 122,346 个 epoch:

正如the Wikipedia article 解释的那样,感知器收敛所需的时期数与向量大小的平方成正比,与边距的平方成反比。在我的数据中,向量的大小很大,但边距也很大。

我试图理解为什么需要这么多 epoch。

更新:根据 cmets 中的要求,我更新了代码以绘制最近 100 个 epoch 的总错误。剧情如下:

P.S.:将要分布的特征缩放为 N(0,1) 后,算法在两个 epoch 后收敛。但是,我不明白为什么即使没有这样的缩放,算法也不会在合理的时间内收敛。

【问题讨论】:

@AlwaysLearning,它已被解释为here,但缩放有助于梯度收敛并有望避免饱和。这意味着它避免了权重接近 1 并简单地重复先前的输入而不学习任何东西。这更像是计算中的一个数值问题。 @AlwaysLearning 如果你愿意,你可以看一下MLP 的 scikit learn 实现并检查他们是如何做到的,那里还有大量的教程,简单明了Python 或 numpy @AlwaysLearning 你的激活函数好像也很奇怪,你是要ReLu吗?如果是这种情况,那么它应该在 x >= 0 时返回 x,否则返回 0,而不是 1。另一件事,你的学习率是疯狂的,1 不应该是学习率,更像是0.1 奇怪,尝试将第一个权重 W 初始化为随机权重向量而不是零。 self.W = np.random.randn(input_size+1) @jhill515 在显示的代码中,total_error 是一个整数(错误分类样本的数量),不是吗? 【参考方案1】:

一个月前我不能正确回答你的问题时,我有点后悔;现在我再试一次。我将旧答案留作记录。

我认为这个问题与损失函数的凸性和局部最小值有关,这使得收敛变得困难。但是,对于您设置的问题,我不太确定您的损失函数的导数,因此我已将您的激活函数修改为 sigmoid,因此我可以轻松应用 log 损失。

这是新的predict

def predict(self, x):
    z = self.W.T.dot(x)
    return 1/(1+np.exp(-z))

这是训练数据的循环,也计算损失。

 loss = 0
 dw = 0
 for i in range(d.shape[0]):
     x = np.insert(X[i], 0, 1)
     y = self.predict(x)
     e = d[i] - (1 if y > 0.5 else 0)
     total_error += np.abs(e)
     dw += self.lr * e * x
     loss2add = (-1) * (np.log(y) if d[i] else np.log(1-y))
     if np.isinf(loss2add) or np.isnan(loss2add):
         loss += 500
     else:
         loss += loss2add
 self.W = self.W + dw
 errors += [total_error]
 losses += [loss/d.shape[0]]

它在 103K epochs 中收敛,所以我希望你相信它的行为与你的初始设置相似。

然后我绘制与W 相关的成本函数。为简单起见,我取已知解的 2 个值,只更改剩余的 1 个值。这是代码(我知道可能更干净):

def predict(W, x):
    z = W.dot(x)
    return 1/(1+np.exp(-z))

trainingSet = np.array([[279.25746446, 162.44072328,   1.        ],
                        [306.23240054, 128.3794866 ,   1.        ],
                        [216.67811217, 148.58167262,   1.        ],
                        [223.64431813, 197.75745016,   1.        ],
                        [486.68209275,  96.09115377,   1.        ],
                        [400.71323154, 125.18183395,   1.        ],
                        [288.87299305, 204.52217766,   1.        ],
                        [245.1492875 ,  55.75847006,  -1.        ],
                        [ 14.95991122, 185.92681911,   1.        ],
                        [393.92908798, 193.40527965,   1.        ],
                        [494.15988362, 179.23456285,   1.        ],
                        [235.59039363, 175.50868526,   1.        ],
                        [423.72071607,   9.50166894,  -1.        ],
                        [ 76.52735621, 208.33663341,   1.        ],
                        [495.1492875 ,  -7.73818431,  -1.        ]])
X = trainingSet[:, :2]
d = trainingSet[:, -1]
d = np.where(d == -1, 1, 0)
losses = []
ws = []
n_points = 10001
for w1 in np.linspace(-40, 40, n_points):
    ws += [w1]
    W = np.array([3629., w1, -238.21109877])
    loss = 0
    for i in range(d.shape[0]):
        x = np.insert(X[i], 0, 1)
        y = predict(W, x)
        loss2add = (-1) * (np.log(y) if d[i] else np.log(1-y))
        if np.isinf(loss2add) or np.isnan(loss2add):
            loss += 500
        else:
            loss += loss2add
    losses += [loss]
plt.plot(ws, losses)
plt.show()

w1 的解决方案是39.48202635。看看损失:

它有一些峰值,因此有一些容易卡住的局部最小值。

但是,如果您将数据置于中心位置

X = scale(X, with_mean=True, with_std=False)

并将 w 设置为

W = np.array([-550.3, w1, -59.65467824])

你得到以下损失函数

在预期区域有最小值(w1 的解是-11.00208344)。

我希望平衡数据集的函数更平滑。

希望现在更清楚了!


在 cmets 之后编辑

这是标准化 -converges 在 26 个 epoch 时的损失函数。

(在这种情况下不居中!)

解约0.7,损失更平滑。标准化与逻辑回归的效果如此之好是有道理的,因为它不会使激活函数的输出饱和。

对于其余部分,关于如何将这些与您提到的理论相适应,我没有什么要补充的。我猜这个定理修复了一个上限,但无论如何都不知道。干杯。

【讨论】:

感谢您的回复。事实上,这看起来是一个富有成效的方向。有两件事有待理解:(1)为什么归一化会去除峰值? (2) 这一切如何符合预测收敛迭代次数而不以特征归一化为条件的理论? 今晚更长的答案。简短回答 (1):在这种情况下,将数据居中比归一化更好。但无论如何,归一化会有所帮助,因为它将使 sigmoid 保持在中心区域,而不是极端,并且损失会更平滑。 (2) 不熟悉这样的理论对不起;显然,在实践中,特征值会影响损失函数,这就是为什么归一化、居中和平衡数据通常会有所帮助,人们推荐它。但我无法为此提供理论。我只能试着用一个例子来证明。 问题中引用了该理论。我会很感激更长的答案。谢谢!【参考方案2】:

您面临的问题可以用一个简单的语句来概括:您的示例数量不利于收敛或您的感知器。

老实说,我不确定从您的综合示例中究竟可以学到什么;无论如何,请不要误会我的意思,在实验室里玩耍并从中学习总是那么好。在拟合神经网络时,有许多通用建议,其中一些建议反映在您的问题的 cmets 中。 This paper 是旧的但很好,你会看到它被引用。

特别是关于您的问题:这实际上不是标准化而是居中的问题。问题是当你重新评估你的体重时

self.W = self.W + self.lr * e * x

您的错误术语 e 将是 +1 或 -1,具体取决于您错误分类的示例(例如,如果示例目标为 1 且分类为 0,则为 +1),但大多数情况下为 +1,因为有是更积极的类,你的坐标在x 和大部分是正值。因此,大多数情况下,您将将权重累加,而不是减去,这样感知器显然会很慢地找到解决方案。

如果你只是扩展你的X

X = scale(X, with_mean=True, with_std=False)

收敛只需要 1461 个 epoch。

分类器是这样的

边界对正类非常接近是有道理的,因为它们有很多;一旦感知器正确处理了所有的正类,工作就差不多完成了。

另外,如果你重新平衡你的数据 - 我已经以这种懒惰的方式完成它作为测试

trainingSet = np.array([[279.25746446, 162.44072328,   1.        ],
                        [306.23240054, 128.3794866 ,   1.        ],
                        [216.67811217, 148.58167262,   1.        ],
                        [223.64431813, 197.75745016,   1.        ],
                        [486.68209275,  96.09115377,   1.        ],
                        [400.71323154, 125.18183395,   1.        ],
                        [288.87299305, 204.52217766,   1.        ],
                        [245.1492875 ,  55.75847006,  -1.        ],
                        [245.1492875 ,  55.75847006,  -1.        ],
                        [245.1492875 ,  55.75847006,  -1.        ],
                        [245.1492875 ,  55.75847006,  -1.        ],
                        [ 14.95991122, 185.92681911,   1.        ],
                        [393.92908798, 193.40527965,   1.        ],
                        [494.15988362, 179.23456285,   1.        ],
                        [235.59039363, 175.50868526,   1.        ],
                        [423.72071607,   9.50166894,  -1.        ],
                        [423.72071607,   9.50166894,  -1.        ],
                        [423.72071607,   9.50166894,  -1.        ],
                        [423.72071607,   9.50166894,  -1.        ],
                        [423.72071607,   9.50166894,  -1.        ],
                        [ 76.52735621, 208.33663341,   1.        ],
                        [495.1492875 ,  -7.73818431,  -1.        ],
                        [495.1492875 ,  -7.73818431,  -1.        ],
                        [495.1492875 ,  -7.73818431,  -1.        ],
                        [495.1492875 ,  -7.73818431,  -1.        ]])

得到这个分类器需要 2 个 epoch(令人惊讶)

希望对您有所帮助。


在 cmets 之后编辑

(1) 关于只加或减的错误

我们以正类为例

[279.25746446, 162.44072328,   1.        ]

对于这些,由于d等于0,所以e只能在分类器正确时为0,在分类器错误时为-1。

e = d[i] - self.predict(x)

(predict 返回 0 或 1)

当权重相加时,如果分类器正确,则什么也不增加,如果错误,则为 -1 * x * 学习率。对于这个例子,假设lr == 1,如果这个正例中有错误,它将正好减去(1, 279.25746446, 162.44072328)

现在,看看所有的正面例子。如果不变换 X,所有坐标都是正值,因此所有分类误差都会减去权重。

现在我们举个反面例子:

[245.1492875 ,  55.75847006,  -1.        ]

对于这些,由于d 等于1,所以e 只能在分类器正确时为0,如果分类器错误则为+1。同样,所有坐标都是正的,除了第三个负例中的一个坐标。因此,几乎所有负类的错误都会被添加。

但是负类的例子只有 3 个,正类的例子有 12 个。因此,错误将主要是减去而不是添加到权重。 (对不起,我在编辑之前把它放在了我的文本中)。有理由认为,如果你什么都不做,收敛会很慢,如果你集中数据,收敛会更快。 (人们甚至会想知道它是如何收敛的。)

(2)关于重采样

我的意思是说重采样(和居中)的收敛速度惊人地快,2 个 epoch。然而,重采样使收敛更快是合理的,因为在将输出拉到一个方向或另一个方向的误差之间有更多的平衡。

希望现在更清楚了。


在更多 cmets 后编辑

我知道样本之间平衡的重要性以及它们如何拉出解决方案并不是很直观。实际上,我面对你的问题的方式可能是相反的:通过查看你的损失函数,思考问题可能是什么,以及我过去遇到的类似问题和我的直觉,我想到了重新平衡 - 然后尝试relabalance 然后将数据居中并确认我对您的损失函数的直觉。直到后来我才试图为你建立一个解释。

当然,并不是我在脑海中处理损失函数并且知道它在做什么。无论如何,我建议你建立自己的直觉,因为你的目标是学习,你可以这样做:绘制分隔线如何在一个时期后移动。

来自您的代码:

labels = [1, 0]
labelColors = ['blue', 'green']

def showData(X, y, plt = plt): 
    colors = [(labelColors[0] if el == labels[0] else labelColors[1]) for el in y] 
    plt.scatter(X[:,0],X[:,1],c=colors)

def plotW(xs, w):
    plt.plot(xs, (w[0] + w[1] * xs)/-w[2], color = 'red', linewidth=4)

import numpy as np
from matplotlib import pyplot as plt
from sklearn.preprocessing import scale

class Perceptron(object):
    """Implements a perceptron network"""
    def __init__(self, input_size, lr=0.1, epochs=1000000):
        self.W = np.zeros(input_size+1)
        #self.W = np.random.randn(input_size+1)
        # add one for bias
        self.epochs = epochs
        self.lr = lr

    def predict(self, x):
        z = self.W.T.dot(x)
        return [1 if self.W.T.dot(x) >=0 else 0]

    def fit(self, X, d):
        errors = []
        for epoch in range(self.epochs):
            if (epoch + 1) % 10000 == 0: print('Epoch',epoch + 1)
            total_error = 0
            for i in range(d.shape[0]):
                x = np.insert(X[i], 0, 1)
                y = self.predict(x)
                e = d[i] - y
                total_error += np.abs(e)
                self.W = self.W + self.lr * e * x
                #print('W: ', self.W)
            errors += [total_error]
            showData(X, d)
            plotW(X[:,0], self.W)
            plt.show()
            if epoch == 100:
                break
            if (total_error == 0):
                print('Done after', epoch, 'epochs')
                nPlot = 100
                plt.plot(list(range(len(errors)-nPlot, len(errors))), errors[-nPlot:])
                plt.show()
                break

if __name__ == '__main__':
    trainingSet = np.array([[279.25746446, 162.44072328,   1.        ],
                            [306.23240054, 128.3794866 ,   1.        ],
                            [216.67811217, 148.58167262,   1.        ],
                            [223.64431813, 197.75745016,   1.        ],
                            [486.68209275,  96.09115377,   1.        ],
                            [400.71323154, 125.18183395,   1.        ],
                            [288.87299305, 204.52217766,   1.        ],
                            [245.1492875 ,  55.75847006,  -1.        ],
                            [ 14.95991122, 185.92681911,   1.        ],
                            [393.92908798, 193.40527965,   1.        ],
                            [494.15988362, 179.23456285,   1.        ],
                            [235.59039363, 175.50868526,   1.        ],
                            [423.72071607,   9.50166894,  -1.        ],
                            [ 76.52735621, 208.33663341,   1.        ],
                            [495.1492875 ,  -7.73818431,  -1.        ]])
    X = trainingSet[:, :2]
    X = scale(X, with_mean=True, with_std=False)
    d = trainingSet[:, -1]
    d = np.where(d == -1, 1, 0)
    perceptron = Perceptron(input_size=2)
    perceptron.fit(X, d)
    print(perceptron.W)

并比较不同设置中生产线的演变。如果您比较居中与不居中时的前 100 个 epoch,您会发现,当您不居中数据时,线条往往会在某种循环中颠簸,而居中时线条移动得更顺畅。 (这实际上与降低学习率时通常会得到的效果相同,正如某些人在 cmets 中所建议的那样。)

我并不是说查看这些图是损失函数行为的分析证据。我什至不假装这是对你问题的真正答案。但无论如何,如果它可以帮助您建立直觉,那将是值得的。

有大量关于收敛的工作,正如您可能知道的那样,它已广泛应用于深度学习,因为它是一个关键问题。当然,您已经听说过不同的优化器以及它们如何影响损失函数的收敛性,在深度学习或一般复杂的神经网络中,这些损失函数肯定难以理解,也无法通过分析来解决。

【讨论】:

不,再平衡是个好主意,但 2 个 epoch 可能出奇的低 即再平衡时通常不会得到如此显着的改善 你为什么说我们会一直添加?当直线向正方向移动太多时,误差就会变成负数,不是吗? 另外,再平衡有什么作用? (非常感谢提供简单解释的链接) 好的。暂时没时间了。我会在今天晚些时候尝试澄清这些。

以上是关于为啥单层感知器在没有归一化的情况下收敛这么慢,即使边距很大?的主要内容,如果未能解决你的问题,请参考以下文章

批归一化(Batch Normalization)

为啥需要softmax函数?为啥不简单归一化?

数据变换-归一化与标准化

matlab中啥叫归一化坐标

有关利用libsvm对数据进行归一化的问题。

聚类算法数据预处理——数据归一化