簡體   English   中英

邏輯回歸代碼在 ~43,500 個生成的觀察值以上停止工作

[英]Logistic regression code stops working above ~43,500 generated observations

在對我在 C 中編寫的執行邏輯回歸的代碼進行故障排除時遇到了一些困難。 雖然它似乎適用於較小的半隨機數據集,但它在我通過 43,500 個觀察值(通過調整創建的觀察值數量確定)附近停止工作(例如分配屬於 class 1 的適當概率)。創建 150代碼中使用的功能,我確實將前兩個創建為 function 的觀察次數,所以我不確定這是否是這里的問題,盡管我使用的是雙精度。也許代碼中某處有溢出?

以下代碼應該是獨立的; 它生成 m=50,000 個觀察值和 n=150 個特征。 將 m 設置為低於 43,500 應返回“Percent class 1: 0.250000”,設置為 44,000 或以上將返回“Percent class 1: 0.000000”,而不管 max_iter(我們對 m 個觀測值進行采樣的次數)設置為多少。

第一個特征設置為 1.0 除以觀察總數,如果 class 為 0(觀察的前 75%),否則將觀察指數除以觀察總數。

第二個特征只是指數除以觀察總數。

所有其他功能都是隨機的。

邏輯回歸旨在使用隨機梯度下降,隨機選擇一個觀察指標,使用當前權重計算損失與預測 y 的梯度,並使用梯度和學習率 (eta) 更新權重。

使用與 Python 和 NumPy 相同的初始化,我仍然得到正確的結果,甚至超過 50,000 個觀察值。

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <time.h>

// Compute z = w * x + b
double dlc( int n, double *X, double *coef, double intercept )
{
    double y_pred = intercept;
    for (int i = 0; i < n; i++)
    {
        y_pred += X[i] * coef[i];
    }
    return y_pred;
}

// Compute y_hat = 1 / (1 + e^(-z))
double sigmoid( int n, double alpha, double *X, double *coef, double beta, double intercept )
{
    double y_pred;
    y_pred = dlc(n, X, coef, intercept);
    y_pred = 1.0 / (1.0 + exp(-y_pred));

    return y_pred;
}

// Stochastic gradient descent
void sgd( int m, int n, double *X, double *y, double *coef, double *intercept, double eta, int max_iter, int fit_intercept, int random_seed )
{
    double *gradient_coef, *X_i;
    double y_i, y_pred, resid;
    int idx;

    double gradient_intercept = 0.0, alpha = 1.0, beta = 1.0;

    X_i = (double *) malloc (n * sizeof(double));
    gradient_coef = (double *) malloc (n * sizeof(double));

    for ( int i = 0; i < n; i++ )
    {
        coef[i] = 0.0;
        gradient_coef[i] = 0.0;
    }
    *intercept = 0.0;

    srand(random_seed);
    
    for ( int epoch = 0; epoch < max_iter; epoch++ )
    {
        for ( int run = 0; run < m; run++ )
        {
            // Randomly sample an observation
            idx = rand() % m;
            for ( int i = 0; i < n; i++ )
            {
                X_i[i] = X[n*idx+i];
            }
            y_i = y[idx];
            // Compute y_hat
            y_pred = sigmoid( n, alpha, X_i, coef, beta, *intercept );
            resid = -(y_i - y_pred);
            // Compute gradients and adjust weights
            for (int i = 0; i < n; i++)
            {
                gradient_coef[i] = X_i[i] * resid;
                coef[i] -= eta * gradient_coef[i];
            }
            if ( fit_intercept == 1 )
            {
                *intercept -= eta * resid;
            }
        }
    }
}

int main(void)
{
    double *X, *y, *coef, *y_pred;
    double intercept;
    double eta = 0.05;
    double alpha = 1.0, beta = 1.0;
    long m = 50000;
    long n = 150;
    int max_iter = 20;

    long class_0 = (long)(3.0 / 4.0 * (double)m);
    double pct_class_1 = 0.0;

    clock_t test_start;
    clock_t test_end;
    double test_time;

    printf("Constructing variables...\n");
    X = (double *) malloc (m * n * sizeof(double));
    y = (double *) malloc (m * sizeof(double));
    y_pred = (double *) malloc (m * sizeof(double));
    coef = (double *) malloc (n * sizeof(double));

    // Initialize classes
    for (int i = 0; i < m; i++)
    {
        if (i < class_0)
        {
            y[i] = 0.0;
        }
        else
        {
            y[i] = 1.0;
        }
    }

    // Initialize observation features
    for (int i = 0; i < m; i++)
    {
        if (i < class_0)
        {
            X[n*i] = 1.0 / (double)m;
        }
        else
        {
            X[n*i] = (double)i / (double)m;
        }
        X[n*i + 1] = (double)i / (double)m;
        for (int j = 2; j < n; j++)
        {
            X[n*i + j] = (double)(rand() % 100) / 100.0;
        }
    }

    // Fit weights
    printf("Running SGD...\n");
    test_start = clock();
    sgd( m, n, X, y, coef, &intercept, eta, max_iter, 1, 42 );
    test_end = clock();
    test_time = (double)(test_end - test_start) / CLOCKS_PER_SEC;
    printf("Time taken: %f\n", test_time);

    // Compute y_hat and share of observations predicted as class 1
    printf("Making predictions...\n");
    for ( int i = 0; i < m; i++ )
    {
        y_pred[i] = sigmoid( n, alpha, &X[i*n], coef, beta, intercept );
    }

    printf("Printing results...\n");
    for ( int i = 0; i < m; i++ )
    {
        //printf("%f\n", y_pred[i]);
        if (y_pred[i] > 0.5)
        {
            pct_class_1 += 1.0;
        }
        // Troubleshooting print
        if (i < 10 || i > m - 10)
        {
            printf("%g\n", y_pred[i]);
        }
    }
    printf("Percent class 1: %f", pct_class_1 / (double)m);

    return 0;
}

作為參考,這是我的(大概)等效的 Python 代碼,它返回超過 50,000 次觀察的正確百分比的已識別類:

import numpy as np
import time

def sigmoid(x):
    return 1 / (1 + np.exp(-x))


class LogisticRegressor:
    def __init__(self, eta, init_runs, fit_intercept=True):
        self.eta = eta
        self.init_runs = init_runs
        self.fit_intercept = fit_intercept
    
    def fit(self, x, y):
        m, n = x.shape
        self.coef = np.zeros((n, 1))
        self.intercept = np.zeros((1, 1))
        
        for epoch in range(self.init_runs):
            for run in range(m):
                idx = np.random.randint(0, m)
                x_i = x[idx:idx+1, :]
                y_i = y[idx]
                y_pred_i = sigmoid(x_i.dot(self.coef) + self.intercept)
                gradient_w = -(x_i.T * (y_i - y_pred_i))
                self.coef -= self.eta * gradient_w
                if self.fit_intercept:
                    gradient_b = -(y_i - y_pred_i)
                    self.intercept -= self.eta * gradient_b
        
    def predict_proba(self, x):
        m, n = x.shape
        y_pred = np.ones((m, 2))
        y_pred[:,1:2] = sigmoid(x.dot(self.coef) + self.intercept)
        y_pred[:,0:1] -= y_pred[:,1:2]
        return y_pred
    
    def predict(self, x):
        return np.round(sigmoid(x.dot(self.coef) + self.intercept))
    

m = 50000
n = 150
class1 = int(3.0 / 4.0 * m)

X = np.random.rand(m, n)
y = np.zeros((m, 1))

for obs in range(m):
    if obs < class1:
        continue
    else:
        y[obs,0] = 1

for obs in range(m):
    if obs < class1:
        X[obs, 0] = 1.0 / float(m)
    else:
        X[obs, 0] = float(obs) / float(m)
    X[obs, 1] = float(obs) / float(m)

logit = LogisticRegressor(0.05, 20)
start_time = time.time()
logit.fit(X, y)
end_time = time.time()
print(round(end_time - start_time, 2))
y_pred = logit.predict(X)
print("Percent:", y_pred.sum() / len(y_pred))

問題在這里:

 // Randomly sample an observation idx = rand() % m;

...鑒於 OP 的RAND_MAX是 32767 這一事實。所有 class 0 觀察結果都在末尾這一事實加劇了這一點。

所有樣本將從前 32768 個觀測值中抽取,當觀測值總數大於此值時,class 0 個觀測值在可采樣的觀測值中的比例小於 0.25。 在總共 43691 個觀測值中,可抽樣的觀測值中沒有class 0 個觀測值。

作為次要問題,如果m不均勻划分RAND_MAX + 1rand() % m不會產生完全均勻的分布,盡管這個問題的影響會更加微妙。


底線:你需要一個更好的隨機數生成器。

至少,您可以考慮將兩次調用rand()的位組合起來以產生具有足夠范圍的 integer,但您可能需要考慮獲取第三方生成器。 有幾個可用。

注意:OP 報告“m=50,000 個觀察值,n=150 個特征。”,所以這可能不是 OP 的問題,但我會在 OP 嘗試更大的任務時留下這個答案以供參考。


一個潛在的問題:

long溢出

m * n * sizeof(double)long為 32 位且m*n > LONG_MAX (或者如果m, n相同時約為 46,341)時存在溢出風險。

OP確實報告

第一步是使用size_t數學執行乘法,我們在計算中至少多獲得 1 位。

// m * n * sizeof(double)
sizeof(double) * m * n 

然而,除非 OP 的size_t超過 32 位,否則我們仍然有麻煩。

IAC,我建議使用size_t來調整數組大小和索引。


還要檢查分配是否失敗。

由於RAND_MAX可能太小,數組索引應該使用size_t數學來完成,考慮一個助手 function 在整個size_t范圍內生成一個隨機索引。

// idx = rand() % m;
size_t idx = rand_size_t() % (size_t)m;

如果堅持使用標准rand() ,下面是一個幫助程序 function 以根據需要擴展其范圍。
它使用真正漂亮的IMAX_BITS(m)

#include <assert.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>

// https://stackoverflow.com/a/4589384/2410359
/* Number of bits in inttype_MAX, or in any (1<<k)-1 where 0 <= k < 2040 */
#define IMAX_BITS(m) ((m)/((m)%255+1) / 255%255*8 + 7-86/((m)%255+12))

// Test that RAND_MAX is a power of 2 minus 1
_Static_assert((RAND_MAX & 1) && ((RAND_MAX/2 + 1) & (RAND_MAX/2)) == 0, "RAND_MAX is not a Mersenne number");

#define RAND_MAX_WIDTH (IMAX_BITS(RAND_MAX))
#define SIZE_MAX_WIDTH (IMAX_BITS(SIZE_MAX))

size_t rand_size_t(void) {
  size_t index = (size_t) rand();
  for (unsigned i = RAND_MAX_WIDTH; i < SIZE_MAX_WIDTH; i += RAND_MAX_WIDTH) {
      index <<= RAND_MAX_WIDTH;
      index ^= (size_t) rand();
  }
  return index;
}

進一步考慮可以將rand_size_t() % (size_t)m替換為更均勻的分布

正如已在其他地方確定的那樣,問題是由於實現的RAND_MAX值太小。

假設 32 位int s,可以在代碼中實現稍微好一點的 PRNG function,例如 C++ 的minstd_rand() function 的 C 實現:

#define MINSTD_RAND_MAX 2147483646

// Code assumes `int` is at least 32 bits wide.

static unsigned int minstd_seed = 1;

static void minstd_srand(unsigned int seed)
{
    seed %= 2147483647;
    // zero seed is bad!
    minstd_seed = seed ? seed : 1;
}

static int minstd_rand(void)
{
    minstd_seed = (unsigned long long)minstd_seed * 48271 % 2147483647;
    return (int)minstd_seed;
}

另一個問題是,當m不除(unsigned int)RAND_MAX + 1時, rand() % m形式的表達式會產生有偏差的結果。 這是一個無偏 function,它返回一個從 0 到le的隨機 integer,使用之前定義的minstd_rand() function:

static int minstd_rand_max(int le)
{
    int r;

    if (le < 0)
    {
        r = le;
    }
    else if (le >= MINSTD_RAND_MAX)
    {
        r = minstd_rand();
    }
    else
    {
        int rm = MINSTD_RAND_MAX - le + MINSTD_RAND_MAX % (le + 1);

        while ((r = minstd_rand()) > rm)
        {
        }
        r /= (rm / (le + 1) + 1);
    }
    return r;
}

(實際上,它仍然有很小的偏差,因為minstd_rand()永遠不會返回 0。)

例如,將rand() % 100替換為minstd_rand_max(99) ,將rand() % m替換為minstd_rand_max(m - 1) 同時將srand(random_seed)替換為minstd_srand(random_seed)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM