Skip to content

Commit 9bf7d8b

Browse files
committed
rewriting codes
1 parent 0cafa37 commit 9bf7d8b

File tree

2 files changed

+370
-0
lines changed

2 files changed

+370
-0
lines changed

doc/src/week35/programs/Lasso.txt

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
2+
LASSO Regression from Scratch with Coordinate Descent (Python)
3+
4+
5+
6+
1. Generate a Synthetic Dataset
7+
8+
9+
To demonstrate LASSO regression, we first create a synthetic linear dataset with a sparse true coefficient vector. This means only a few features actually influence the target, while the rest have zero true effect. Below, we generate N = 100 data points with p = 10 features. We choose true coefficients for only a subset of features (e.g. features 0, 1, and 4) and set others to zero. The target y is then computed as a linear combination of the features plus some Gaussian noise. This gives us a dataset where only the chosen features have a real relationship to y (the signal), and the rest are irrelevant (noise).
10+
import numpy as np
11+
12+
# Seed for reproducibility
13+
np.random.seed(0)
14+
15+
# Dimensions of the synthetic dataset
16+
N = 100 # number of samples (observations)
17+
p = 10 # number of features
18+
19+
# True sparse coefficients (only a few non-zero)
20+
w_true = np.array([5, -3, 0, 0, 2, 0, 0, 0, 0, 0], dtype=float)
21+
# For example, feature 0 has coefficient 5, feature 1 has -3, feature 4 has 2, rest are 0.
22+
23+
# Generate feature matrix X from a normal distribution
24+
X = np.random.randn(N, p)
25+
26+
# Generate target values: linear combination of X with w_true + noise
27+
noise = np.random.randn(N) * 1.0 # noise with standard deviation 1.0
28+
y = X.dot(w_true) + noise
29+
30+
2. Data Preprocessing (Normalization)
31+
32+
33+
LASSO (and linear regression in general) benefits from feature scaling. We will standardize the feature matrix X so that each feature has mean 0 and unit variance. This ensures the L1 penalty affects all features more equally and helps the coordinate descent algorithm converge. We also center the target y to mean 0. Centering y allows us to ignore the intercept term in the regression (the model will implicitly handle the intercept as 0 after centering).
34+
# Standardize features (zero mean, unit variance for each column)
35+
X_mean = X.mean(axis=0)
36+
X_std = X.std(axis=0)
37+
X_std[X_std == 0] = 1.0 # avoid division by zero if any constant feature
38+
X_norm = (X - X_mean) / X_std
39+
40+
# Center the target to zero mean
41+
y_mean = y.mean()
42+
y_centered = y - y_mean
43+
After this preprocessing, each column of X_norm has mean ~0 and std ~1, and y_centered has mean ~0.
44+
45+
46+
3. Implement LASSO Regression via Coordinate Descent
47+
48+
49+
LASSO regression optimizes the objective:
50+
51+
$$\min_{w} ; \frac{1}{2}|y - Xw|^2 + \alpha \sum_{j}|w_j|,$$
52+
53+
where $\alpha$ is the regularization strength (sometimes denoted $\lambda$). The L1 penalty term $\sum_j |w_j|$ induces sparsity in the solution, forcing some coefficients exactly to zero.
54+
55+
Coordinate Descent Algorithm: LASSO has no closed-form solution due to the non-differentiable L1 term, but we can solve it iteratively by coordinate descent . Coordinate descent optimizes one coefficient $w_j$ at a time, keeping others fixed, and cycles through features repeatedly until convergence. For each feature $j$, we find the optimal $w_j$ that minimizes the objective while treating all other $w_{k\neq j}$ as constants. This leads to a soft-thresholding update formula:
56+
57+
First, compute the partial residual (excluding feature $j$):
$$\rho_j = \sum_{i} x_{ij}\Big( y_i - \sum_{k \neq j} x_{ik} w_k \Big),$$
which is essentially the correlation between feature $j$ and the current residual. (If data is normalized, $\rho_j = x_j^T (y - X_{-j}w_{-j})$.)
58+
Then update $w_j$ by applying the soft-threshold function to $\rho_j$:
$$w_j \leftarrow \frac{1}{\sum_i x_{ij}^2} ; S(\rho_j,; \alpha),$$
where $S(\rho,\alpha) = \operatorname{sign}(\rho)\max(|\rho| - \alpha,,0)$ is the soft-thresholding operator. This operator shrinks $\rho_j$ by $\alpha$ and sets $w_j$ to zero if $|\rho_j| \le \alpha$. The division by $\sum_i x_{ij}^2$ accounts for the scale of feature $j$ (for standardized data, this is just $N$, or 1 if variance=1) .
59+
60+
61+
Below, we implement the LASSO fitting using coordinate descent. We define a helper soft_threshold function and then perform cyclic updates of each coefficient until convergence. We consider the algorithm converged when the maximum change in any coefficient in an iteration is below a small tolerance (tol).
62+
import numpy as np
63+
64+
def soft_threshold(rho, lam):
65+
"""Soft thresholding operator: S(rho, lam) = sign(rho)*max(|rho|-lam, 0)."""
66+
if rho < -lam:
67+
return rho + lam
68+
elif rho > lam:
69+
return rho - lam
70+
else:
71+
return 0.0
72+
73+
def lasso_coordinate_descent(X, y, alpha, max_iter=1000, tol=1e-6):
74+
"""
75+
Perform LASSO regression using coordinate descent.
76+
X : array of shape (n_samples, n_features), assumed to be standardized.
77+
y : array of shape (n_samples,), assumed centered.
78+
alpha : regularization strength (L1 penalty coefficient).
79+
max_iter : maximum number of coordinate descent iterations (full cycles).
80+
tol : tolerance for convergence (stop if max coef change < tol).
81+
"""
82+
n_samples, n_features = X.shape
83+
w = np.zeros(n_features) # initialize weights to zero
84+
for it in range(max_iter):
85+
w_old = w.copy()
86+
# Loop over each feature coordinate
87+
for j in range(n_features):
88+
# Compute rho_j = x_j^T (y - X w + w_j * x_j)
89+
# (This is the contribution of feature j to the residual)
90+
X_j = X[:, j]
91+
# temporarily exclude feature j's effect
92+
residual = y - X.dot(w) + w[j] * X_j
93+
rho_j = X_j.dot(residual)
94+
# Soft thresholding update for w_j
95+
w[j] = soft_threshold(rho_j, alpha) / (X_j.dot(X_j))
96+
# Check convergence: if all updates are very small, break
97+
if np.max(np.abs(w - w_old)) < tol:
98+
break
99+
return w
100+
In the code above, for each feature $j$, we compute rho_j as the dot product of feature column X_j with the current residual (with $w_j$’s contribution added back). Then we apply the soft-threshold update. The result is that $w_j$ will be pulled towards 0 by an amount $\alpha$; if $\rho_j is smaller than $\alpha in magnitude, $w_j` becomes 0 (feature eliminated). This is what gives LASSO the ability to perform feature selection.
101+
102+
103+
4. Fit the Model on the Synthetic Dataset
104+
105+
106+
Now we use our lasso_coordinate_descent function to fit the model on the synthetic data. We need to choose a regularization parameter α. This hyperparameter controls how strongly we penalize large weights: a larger α yields more sparsity (more coefficients forced to zero), while a smaller α yields a solution closer to ordinary least squares.
107+
108+
For this example, we choose a moderate value of α (e.g. 50.0) that is large enough to shrink or zero-out the irrelevant features, but not so large that it completely zeroes out the smaller true coefficients. In practice, α could be tuned via cross-validation, but here we just pick a value for demonstration.
109+
alpha = 50.0 # regularization strength
110+
w_learned = lasso_coordinate_descent(X_norm, y_centered, alpha)
111+
112+
print("True coefficients:", w_true)
113+
print("Learned coefficients:", w_learned)
114+
Running the above, we obtain the learned weight vector. We expect the algorithm to recover the pattern that features 0, 1, and 4 have the largest influence. The irrelevant features (with true coefficient 0) should end up with coefficients near or exactly zero due to the L1 penalty.
115+
116+
117+
5. Results and Visualization
118+
119+
120+
To verify our implementation, we will visualize several aspects of the results:
121+
122+
Synthetic Data Relationships: We plot the target variable against one relevant feature and one irrelevant feature to illustrate the presence or absence of linear correlation in the data.
123+
Convergence Plot: We track the LASSO cost function value over iterations to ensure that the coordinate descent algorithm is converging.
124+
True vs Learned Coefficients: We compare the final learned coefficients to the true coefficients to see if the LASSO model identified the correct sparse pattern.
125+
126+
127+
Synthetic data scatter plots. The figure above shows the relationship between the target y and two example features. Left: Feature 0 (which has a true coefficient of 5) exhibits a clear linear correlation with y – as feature 0 increases, the target tends to increase as well. Right: Feature 2 (true coefficient 0) shows no evident correlation with y; the points are scattered without a trend. This reflects that feature 2 is irrelevant (pure noise) in the data generation. In our synthetic dataset, only a few features (like feature 0) have a real effect on y, making the true model sparse.
128+
import matplotlib.pyplot as plt
129+
130+
# Plot y vs a relevant feature (0) and an irrelevant feature (2)
131+
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
132+
axes[0].scatter(X[:, 0], y, color='blue', alpha=0.6)
133+
axes[0].set_title("Feature 0 (Relevant) vs Target")
134+
axes[0].set_xlabel("Feature 0 values")
135+
axes[0].set_ylabel("Target (y)")
136+
axes[1].scatter(X[:, 2], y, color='red', alpha=0.6)
137+
axes[1].set_title("Feature 2 (Irrelevant) vs Target")
138+
axes[1].set_xlabel("Feature 2 values")
139+
axes[1].set_ylabel("Target (y)")
140+
plt.tight_layout()
141+
plt.show()
142+
LASSO cost function decrease over iterations. The plot above shows the value of the objective (cost) function as the coordinate descent proceeds. We see that the cost drops dramatically in the first iteration and then continues to decrease, leveling off by around 5–7 iterations. In fact, for this problem the algorithm converged in only a few passes over the features. This rapid convergence indicates that the coordinate descent algorithm is efficiently optimizing the LASSO objective – after a big initial improvement, subsequent iterations make only minor refinements as it reaches the minimum. (The first iteration has the largest drop because the initial weights were all zero, so the first updates capture most of the variance in y.)
143+
# Track cost history during coordinate descent for plotting
144+
def lasso_with_cost_history(X, y, alpha, max_iter=1000):
145+
n_samples, n_features = X.shape
146+
w = np.zeros(n_features)
147+
cost_history = []
148+
# initial cost
149+
cost_history.append(0.5 * np.sum((y - X.dot(w))**2) + alpha * np.sum(np.abs(w)))
150+
for it in range(max_iter):
151+
w_old = w.copy()
152+
for j in range(n_features):
153+
X_j = X[:, j]
154+
residual = y - X.dot(w) + w[j] * X_j
155+
rho_j = X_j.dot(residual)
156+
w[j] = soft_threshold(rho_j, alpha) / (X_j.dot(X_j))
157+
# compute cost after this iteration
158+
cost = 0.5 * np.sum((y - X.dot(w))**2) + alpha * np.sum(np.abs(w))
159+
cost_history.append(cost)
160+
if np.max(np.abs(w - w_old)) < 1e-6:
161+
break
162+
return w, cost_history
163+
164+
# Run coordinate descent and get cost history
165+
w_fit, cost_history = lasso_with_cost_history(X_norm, y_centered, alpha=50.0)
166+
167+
# Plot cost vs iteration
168+
plt.figure(figsize=(6,4))
169+
plt.plot(cost_history, marker='o', color='purple')
170+
plt.title("LASSO Cost Decrease over Iterations")
171+
plt.xlabel("Iteration")
172+
plt.ylabel("Cost function value")
173+
plt.grid(True)
174+
plt.show()
175+
True vs learned regression coefficients. The bar chart above compares the true coefficients (orange/yellow bars) used to generate the data with the coefficients learned by our LASSO model (blue/red bars). The LASSO regression successfully recovered the sparse pattern:
176+
177+
Features 0, 1, and 4 (which truly had non-zero effects) are assigned significant weights by the model. For example, feature 0’s true coefficient is 5, and the model learned ~4.48; feature 1’s true value is -3, learned ~-2.26; feature 4’s true value is 2, learned ~1.21. The learned values are slightly shrunk towards zero compared to the true values due to the L1 penalty (this is the expected shrinkage effect of LASSO).
178+
All other features (indices 2, 3, 5, 6, 7, 8, 9), which had true coefficient 0, are given learned coefficients extremely close to 0. In fact, most of these ended up exactly 0, meaning the model correctly eliminated those features as irrelevant.
179+
180+
# Compare true vs learned coefficients
181+
import numpy as np
182+
import matplotlib.pyplot as plt
183+
184+
indices = np.arange(p)
185+
width = 0.4
186+
plt.figure(figsize=(6,4))
187+
plt.bar(indices - width/2, w_true, width=width, label='True Coefficient')
188+
plt.bar(indices + width/2, w_fit, width=width, label='Learned Coefficient')
189+
plt.xlabel("Feature index")
190+
plt.ylabel("Coefficient value")
191+
plt.title("True vs Learned Coefficients")
192+
plt.legend()
193+
plt.show()
194+
As we can see, the implementation from scratch is able to recover the underlying sparse relationship in the data. Our LASSO model picked out the correct relevant features and shrank the rest to zero. This example illustrates how LASSO regression performs feature selection and how the coordinate descent algorithm converges to the solution. The full code above is a complete, easy-to-run script that generates data, normalizes it, fits a LASSO model, and produces visualizations of the results.

0 commit comments

Comments
 (0)