[英]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 + 1
, rand() % 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.