1"""
2Linear Regression - NumPy From-Scratch ๊ตฌํ
3
4์ด ํ์ผ์ ์ ํ ํ๊ท๋ฅผ ์์ NumPy๋ก ๊ตฌํํฉ๋๋ค.
5ํ๋ ์์ํฌ ์์ด gradient descent๋ฅผ ์ง์ ๊ตฌํํ์ฌ
6๋ฅ๋ฌ๋์ ๊ธฐ๋ณธ ์๋ฆฌ๋ฅผ ์ดํดํฉ๋๋ค.
7
8ํ์ต ๋ชฉํ:
91. Forward pass: y_hat = Xw + b
102. Loss ๊ณ์ฐ: MSE = (1/2n) * ||y - y_hat||^2
113. Backward pass: gradient ๊ณ์ฐ
124. Weight update: w = w - lr * dw
13"""
14
15import numpy as np
16import matplotlib.pyplot as plt
17
18
19class LinearRegressionNumpy:
20 """
21 NumPy๋ก ๊ตฌํํ Linear Regression
22
23 ์ํ์ ๋ฐฐ๊ฒฝ:
24 - ๋ชจ๋ธ: ลท = Xw + b
25 - ์์ค: L = (1/2n) ฮฃ(y - ลท)ยฒ
26 - ๊ทธ๋๋์ธํธ:
27 โL/โw = (1/n) X^T (ลท - y)
28 โL/โb = (1/n) ฮฃ(ลท - y)
29 """
30
31 def __init__(self, input_dim: int, output_dim: int = 1):
32 """
33 Args:
34 input_dim: ์
๋ ฅ ํน์ฑ ์
35 output_dim: ์ถ๋ ฅ ์ฐจ์ (๊ธฐ๋ณธ 1)
36 """
37 # Xavier/He ์ด๊ธฐํ: ๋ถ์ฐ์ 2/n์ผ๋ก ์ ์ง
38 self.W = np.random.randn(input_dim, output_dim) * np.sqrt(2.0 / input_dim)
39 self.b = np.zeros((1, output_dim))
40
41 # ๊ทธ๋๋์ธํธ ์ ์ฅ์ฉ
42 self.dW = None
43 self.db = None
44
45 # Forward์์ ์บ์ (backward์์ ์ฌ์ฉ)
46 self._cache = {}
47
48 def forward(self, X: np.ndarray) -> np.ndarray:
49 """
50 Forward pass: ลท = Xw + b
51
52 Args:
53 X: ์
๋ ฅ ๋ฐ์ดํฐ (batch_size, input_dim)
54
55 Returns:
56 y_hat: ์์ธก๊ฐ (batch_size, output_dim)
57 """
58 # ์
๋ ฅ ์บ์ (backward์์ ํ์)
59 self._cache['X'] = X
60
61 # ์ ํ ๋ณํ: y = Xw + b
62 y_hat = np.dot(X, self.W) + self.b
63
64 return y_hat
65
66 def compute_loss(self, y: np.ndarray, y_hat: np.ndarray) -> float:
67 """
68 Mean Squared Error ์์ค ๊ณ์ฐ
69
70 L = (1/2n) ฮฃ(y - ลท)ยฒ
71
72 Args:
73 y: ์ค์ ๊ฐ (batch_size, output_dim)
74 y_hat: ์์ธก๊ฐ (batch_size, output_dim)
75
76 Returns:
77 loss: ์ค์นผ๋ผ ์์ค๊ฐ
78 """
79 n = y.shape[0]
80 loss = (1 / (2 * n)) * np.sum((y - y_hat) ** 2)
81 return loss
82
83 def backward(self, y: np.ndarray, y_hat: np.ndarray) -> None:
84 """
85 Backward pass: ๊ทธ๋๋์ธํธ ๊ณ์ฐ
86
87 Chain Rule ์ ์ฉ:
88 โL/โw = โL/โลท ร โลท/โw
89 = (1/n)(ลท - y) ร X^T
90 = (1/n) X^T (ลท - y)
91
92 โL/โb = โL/โลท ร โลท/โb
93 = (1/n) ฮฃ(ลท - y)
94
95 Args:
96 y: ์ค์ ๊ฐ
97 y_hat: ์์ธก๊ฐ
98 """
99 X = self._cache['X']
100 n = y.shape[0]
101
102 # ์ค์ฐจ
103 error = y_hat - y # (batch_size, output_dim)
104
105 # ๊ทธ๋๋์ธํธ ๊ณ์ฐ
106 # โL/โW = (1/n) X^T @ error
107 self.dW = (1 / n) * np.dot(X.T, error)
108
109 # โL/โb = (1/n) ฮฃerror (๊ฐ ์ถ๋ ฅ ์ฐจ์๋ณ)
110 self.db = (1 / n) * np.sum(error, axis=0, keepdims=True)
111
112 def update(self, lr: float) -> None:
113 """
114 ๊ฐ์ค์น ์
๋ฐ์ดํธ (Gradient Descent)
115
116 w = w - ฮท ร โL/โw
117 b = b - ฮท ร โL/โb
118
119 Args:
120 lr: learning rate
121 """
122 self.W -= lr * self.dW
123 self.b -= lr * self.db
124
125 def fit(
126 self,
127 X: np.ndarray,
128 y: np.ndarray,
129 lr: float = 0.01,
130 epochs: int = 1000,
131 verbose: bool = True
132 ) -> list:
133 """
134 ๋ชจ๋ธ ํ์ต
135
136 Args:
137 X: ํ์ต ๋ฐ์ดํฐ (n_samples, n_features)
138 y: ํ๊ฒ๊ฐ (n_samples, 1) ๋๋ (n_samples,)
139 lr: learning rate
140 epochs: ํ์ต ๋ฐ๋ณต ํ์
141 verbose: ์งํ ์ํฉ ์ถ๋ ฅ ์ฌ๋ถ
142
143 Returns:
144 losses: ์ํญ๋ณ ์์ค ๋ฆฌ์คํธ
145 """
146 # y shape ๋ณด์
147 if y.ndim == 1:
148 y = y.reshape(-1, 1)
149
150 losses = []
151
152 for epoch in range(epochs):
153 # 1. Forward pass
154 y_hat = self.forward(X)
155
156 # 2. Loss ๊ณ์ฐ
157 loss = self.compute_loss(y, y_hat)
158 losses.append(loss)
159
160 # 3. Backward pass (gradient ๊ณ์ฐ)
161 self.backward(y, y_hat)
162
163 # 4. Weight update
164 self.update(lr)
165
166 # ์งํ ์ํฉ ์ถ๋ ฅ
167 if verbose and (epoch + 1) % (epochs // 10) == 0:
168 print(f"Epoch {epoch + 1}/{epochs}, Loss: {loss:.6f}")
169
170 return losses
171
172 def predict(self, X: np.ndarray) -> np.ndarray:
173 """์์ธก"""
174 return self.forward(X)
175
176
177def generate_sample_data(n_samples: int = 100, n_features: int = 1, noise: float = 0.1):
178 """
179 ํ
์คํธ์ฉ ์ํ ๋ฐ์ดํฐ ์์ฑ
180
181 y = 2x + 3 + noise
182 """
183 np.random.seed(42)
184 X = np.random.randn(n_samples, n_features)
185
186 # ์ค์ ๊ฐ์ค์น (ํ์ต์ผ๋ก ์ฐพ์์ผ ํ ๊ฐ)
187 true_w = np.array([[2.0]])
188 true_b = 3.0
189
190 y = np.dot(X, true_w) + true_b + noise * np.random.randn(n_samples, 1)
191
192 return X, y, true_w, true_b
193
194
195def main():
196 """๋ฉ์ธ ์คํ ํจ์"""
197 print("=" * 60)
198 print("Linear Regression - NumPy From-Scratch ๊ตฌํ")
199 print("=" * 60)
200
201 # 1. ๋ฐ์ดํฐ ์์ฑ
202 print("\n1. ์ํ ๋ฐ์ดํฐ ์์ฑ")
203 X, y, true_w, true_b = generate_sample_data(n_samples=100, noise=0.1)
204 print(f" X shape: {X.shape}")
205 print(f" y shape: {y.shape}")
206 print(f" True w: {true_w.flatten()}, True b: {true_b}")
207
208 # 2. ๋ชจ๋ธ ์์ฑ
209 print("\n2. ๋ชจ๋ธ ์ด๊ธฐํ")
210 model = LinearRegressionNumpy(input_dim=1, output_dim=1)
211 print(f" Initial W: {model.W.flatten()}")
212 print(f" Initial b: {model.b.flatten()}")
213
214 # 3. ํ์ต
215 print("\n3. ํ์ต ์์")
216 losses = model.fit(X, y, lr=0.1, epochs=100, verbose=True)
217
218 # 4. ๊ฒฐ๊ณผ ํ์ธ
219 print("\n4. ํ์ต ๊ฒฐ๊ณผ")
220 print(f" Learned W: {model.W.flatten()}")
221 print(f" Learned b: {model.b.flatten()}")
222 print(f" True W: {true_w.flatten()}")
223 print(f" True b: {true_b}")
224 print(f" Final Loss: {losses[-1]:.6f}")
225
226 # 5. ์๊ฐํ
227 print("\n5. ์๊ฐํ")
228 fig, axes = plt.subplots(1, 2, figsize=(12, 4))
229
230 # Loss ๊ณก์
231 axes[0].plot(losses)
232 axes[0].set_xlabel('Epoch')
233 axes[0].set_ylabel('Loss (MSE)')
234 axes[0].set_title('Training Loss')
235 axes[0].grid(True)
236
237 # ๋ฐ์ดํฐ์ ์์ธก ์ง์
238 y_pred = model.predict(X)
239 sorted_idx = np.argsort(X.flatten())
240 axes[1].scatter(X, y, alpha=0.5, label='Data')
241 axes[1].plot(X[sorted_idx], y_pred[sorted_idx], 'r-', linewidth=2, label='Prediction')
242 axes[1].set_xlabel('X')
243 axes[1].set_ylabel('y')
244 axes[1].set_title('Linear Regression Fit')
245 axes[1].legend()
246 axes[1].grid(True)
247
248 plt.tight_layout()
249 plt.savefig('linear_regression_result.png', dpi=150)
250 plt.show()
251 print(" ๊ฒฐ๊ณผ ์ด๋ฏธ์ง ์ ์ฅ: linear_regression_result.png")
252
253
254if __name__ == "__main__":
255 main()