Python 从头开始​​实现随机梯度下降。实施是否正确?

Stochastic Gradient Descent implementation in Python from scratch. is the implementation correct?

我知道这看起来与之前就同一主题提出的许多问题相似。我调查了他们中的大多数,但他们并没有完全回答我的问题。我的问题是我的梯度没有收敛到最优值,它甚至在非常低的 alpha 值下发散和振荡。

我的数据生成函数如下

X = [[float(np.random.randn(1)) for i in range(0,100)] for j in range(0,5)]
X = np.array(X).transpose()
Y = [float(0) for i in range(0,100)]
Y = 2*X[:,0] + 3*X[:,1] + 1*X[:,2] + 4*X[:,3] + 1*X[:,4] + 5
fig, ax = plt.subplots(1,5)
fig.set_size_inches(20,5)
k = 0
for j in range(0,5):
    sns.scatterplot(X[:,k],Y,ax=ax[j])
    k += 1

我的 SGD 实现如下

def multilinreg(X,Y,epsilon = 0.000001,alpha = 0.01,K = 20):
    Xnot = [[1] for i in range(0,len(X))]
    Xnot = np.array(Xnot)
    X = np.append(Xnot,X, axis = 1)
    vars = X.shape[1]
    W = []
    W = [np.random.normal(1) for i in range(vars)]
    W = np.array(W)
    J = 0
    for i in range(len(X)):
      Yunit = 0
      for j in range(vars):
        Yunit = Yunit + X[i,j] * W[j]
        J = J + (0.5/(len(X)))*((Y[i]-Yunit)**2)
    err = 1
    iter = 0
    Weights = []
    Weights.append(W)
    Costs = []
    while err > epsilon:
      index = [np.random.randint(len(Y)) for i in range(K)]
      Xsample, Ysample = X[index,:], Y[index]
      m =len(Xsample)
      Ypredsample = []
      for i in range(len(Xsample)):
        Yunit = 0
        for j in range(vars):
          Yunit = Yunit + X[i,j] * W[j]
        Ypredsample.append(Yunit)
      Ypredsample = np.array(Ypredsample)
      for i in range(len(Xsample)):
        for j in range(vars):
          gradJunit = (-1)*(Xsample[i,j]*(Ysample[i] - Ypredsample[i]))
          W[j] = W[j] - alpha*gradJunit
      Jnew = 0
      for i in range(len(Xsample)):
        Yunit = 0
        for j in range(vars):
          Yunit = Yunit + Xsample[i,j]*W[j]
          Jnew = Jnew + (0.5/(len(Xsample)))*((Ysample[i]-Yunit)**2)
      Weights.append(W)
      err = abs(float(Jnew - J))
      J = Jnew 
      Costs.append(J)
      iter += 1
      if iter % 1000 == 0:
        print(iter)
        print(J)
    Costs = np.array(Costs)
    Ypred = []
    for i in range(len(X)):
      Yunit = 0
      for j in range(vars):
        Yunit = Yunit + X[i,j] * W[j]
      Ypred.append(Yunit)
    Ypred = np.array(Ypred)
    return Ypred, iter, Costs, W

超参数如下

epsilon = 1*(10)**(-20)
alpha = 0.0000001
K = 50

我不认为这是一个数据 issue.I 我使用的是相当简单的线性函数。

我认为是方程式,但我也仔细检查了它们,它们对我来说似乎没问题。

您的实施中有几处需要更正(大多数是出于效率原因)。当然,您可以通过简单地定义 w = np.array([5, 2, 3, 1, 4, 1]) 来节省时间,但这并不能回答为什么您的 SGD 实现不起作用的问题。

首先,您通过以下方式定义 X

X = [[float(np.random.randn(1)) for i in range(0,100)] for j in range(0,5)]
X = np.array(X).transpose()

执行此操作的更快方法是:

X = np.random.randn(100, 5)

然后,你定义Y:

Y = [float(0) for i in range(0,100)]
Y = 2*X[:,0] + 3*X[:,1] + 1*X[:,2] + 4*X[:,3] + 1*X[:,4] + 5

第一次初始化 Y = [float(0) for i in range(0,100)] 没用,因为你立即用第二行覆盖了 Y。编写此行的更简洁的方式也可以是:

Y = X @ np.array([2, 3, 1, 4, 1]) + 5

现在,关于您的 SGD 实施。行:

    Xnot = [[1] for i in range(0,len(X))]
    Xnot = np.array(Xnot)
    X = np.append(Xnot,X, axis = 1)

可以更有效地重写为:

    X = np.hstack((np.ones(len(X)).reshape(-1, 1), X))

同样,行

    W = []
    W = [np.random.normal(1) for i in range(vars)]
    W = np.array(W)

可以使用numpy函数重写。请注意,第一行 W = [] 是无用的,因为您在不使用它之后立即覆盖了 Wnp.random.normal 可以使用 size 关键字参数直接生成多于 1 个样本。另外,请注意,当使用 np.random.normal(1) 时,您是从均值 1 和标准差 1 的正态分布中抽样,而您可能想从均值 0 和标准差 1 的正态分布中抽样。因此,您应该定义:

    W = np.random.normal(size=vars)

Yunit 是您使用 W 做出的预测。根据定义,您可以通过执行以下操作来计算它:

    Yunit = X @ W

这避免了嵌套的 for 循环。不过,您计算 J 的方式很奇怪。如果我没记错的话, J 对应于你的损失函数。但是,J 的公式假设 MSE 损失为 J = 0.5 * sum from k=1 to len(X) of (y_k - w*x_k) ** 2。因此,这两个嵌套的 for 循环可以重写为:

    Yunit = X @ W
    J = 0.5 * np.sum((Y - Yunit) ** 2)

顺便说一句:这样命名 err 可能会误导我,因为 error 通常是成本,而它表示此处每个步骤所取得的进展。行:

    Weights = []
    Weights.append(W)

可以改写为:

   Weights = [W]

J 添加到您的 Costs 列表也是合乎逻辑的,因为这是对应于 W:

的列表
    Costs = [J]

由于您想执行随机梯度下降,因此无需随机选择要从数据集中提取的样本。您有两个选择:要么在每个样本处更新权重,要么计算 J w.r.t 的梯度。你的体重。后者实现起来更简单一些,并且通常比前者收敛得更优雅。但是,由于您选择了前者,因此我将使用它。请注意,即使在这个版本中,您也不必随机选择样本,但我将使用与您相同的方法,因为这也应该有效。关于您的抽样,我认为最好确保您不要两次采用相同的索引。因此,您可能需要这样定义 index

    index = np.random.choice(np.arange(len(Y)), size=K, replace=False)

m 没有用,因为在这种情况下它总是等于 K。如果您在执行抽样时没有确保两次没有相同的索引,则应该保留它。如果您想在不检查对同一索引进行两次采样的情况下执行采样,只需将 replace=True 放入 choice 函数中即可。

再一次,您可以使用矩阵乘法更有效地计算 Yunit。因此,您可以替换:

      Ypredsample = []
      for i in range(len(Xsample)):
        Yunit = 0
        for j in range(vars):
          Yunit = Yunit + X[i,j] * W[j]
        Ypredsample.append(Yunit)

作者:

    Ypredsample = X @ W

同样,您可以使用 numpy 函数计算您的权重更新。因此,您可以替换:

      for i in range(len(Xsample)):
        for j in range(vars):
          gradJunit = (-1)*(Xsample[i,j]*(Ysample[i] - Ypredsample[i]))
          W[j] = W[j] - alpha*gradJunit

作者:

    W -= alpha * np.sum((Ypredsample - Ysample).reshape(-1, 1) * Xsample, axis=0)

和以前一样,可以使用矩阵乘法计算成本。但是请注意,您应该在整个数据集上计算 J。因此,您应该替换:

      Jnew = 0
      for i in range(len(Xsample)):
        Yunit = 0
        for j in range(vars):
          Yunit = Yunit + Xsample[i,j]*W[j]
          Jnew = Jnew + (0.5/(len(Xsample)))*((Ysample[i]-Yunit)**2)

作者:

   Jnew = 0.5 * np.sum((Y - X @ W) ** 2)

最后,您可以使用矩阵乘法来进行预测。因此,您的最终代码应如下所示:

import numpy as np

X = np.random.randn(100, 5)
Y = X @ np.array([2, 3, 1, 4, 1]) + 5

def multilinreg(X, Y, epsilon=0.00001, alpha=0.01, K=20):
    X = np.hstack((np.ones(len(X)).reshape(-1, 1), X))
    vars = X.shape[1]
    W = np.random.normal(size=vars)
    Yunit = X @ W
    J = 0.5 * np.sum((Y - Yunit) ** 2)
    err = 1
    Weights = [W]
    Costs = [J]
    iter = 0

    while err > epsilon:
        index = np.random.choice(np.arange(len(Y)), size=K, replace=False)
        Xsample, Ysample = X[index], Y[index]
        Ypredsample = Xsample @ W
        W -= alpha * np.sum((Ypredsample - Ysample).reshape(-1,1) * Xsample, axis=0)
        Jnew = 0.5 * np.sum((Y - X @ W) ** 2)
        Weights.append(Jnew)
        err = abs(Jnew - J)
        J = Jnew
        Costs.append(J)
        iter += 1

        if iter % 10 == 0:
            print(iter)
            print(J)

    Costs = np.array(Costs)
    Ypred = X @ W
    return Ypred, iter, Costs, W

运行 它 returns W=array([4.99956786, 2.00023614, 3.00000213, 1.00034205, 3.99963732, 1.00063196]) 在 61 次迭代中,最终成本为 3.05e-05。

现在我们知道这段代码是正确的,我们可以用它来确定你的错误在哪里。在这段代码中:

      for i in range(len(Xsample)):
        Yunit = 0
        for j in range(vars):
          Yunit = Yunit + X[i,j] * W[j]
        Ypredsample.append(Yunit)
      Ypredsample = np.array(Ypredsample)

你用 X[i, j] 而不是 Xsample[i, j],这没有意义。另外,如果您在循环中打印 W 以及 Jiter,您可以看到程序很快找到了正确的 W(一旦先前的修复已被made),但不会停止,可能是因为 J 计算不正确。错误是这一行:

Jnew = Jnew + (0.5/(len(Xsample)))*((Ysample[i]-Yunit)**2)

缩进不正确。事实上,它不应该是 for j in range(vars) 循环的一部分,而应该只是 for i in range(len(Xsample)) 循环的一部分,像这样:

      Jnew = 0
      for i in range(len(Xsample)):
        Yunit = 0
        for j in range(vars):
          Yunit = Yunit + Xsample[i,j]*W[j]
        Jnew = Jnew + (0.5/(len(Xsample)))*((Ysample[i]-Yunit)**2)

通过更正此问题,您的代码可以正常工作。此错误也会出现在程序的开头,但只要完成两次以上的迭代就不会影响它。