一、什麼是線性迴歸
結合我們生活中例子,如果你是一個水果店老闆,你想知道“草莓的重量”和“它的價格”之間有什麼關係。憑經驗你知道,越重的草莓肯定越貴。線性迴歸就是幫你把這種模糊的經驗,變成一個精確的數學公式。
核心思想:找到一個線性方程(一條直線),來最好地描述自變量 (X)(比如:重量)和因變量 (Y)(比如:價格)之間的關係。
公式:Y = wX + b
- Y: 我們要預測的值(價格)
- X: 我們已知的、用來預測的特徵(重量)
- w: 權重,代表直線的斜率。意思是“X每增加1個單位,Y會增加w個單位”。比如w=5,就是重量每增加1克,價格增加5元。
- b: 偏置,代表直線的截距。意思是“即使草莓重量為0,它也有一個基礎價值”(比如包裝盒的成本),但現實中這個值可能為0或很小。
可視化的理解:我們用Python生成一組模擬數據,直觀感受線性關係:
import numpy as np
import matplotlib.pyplot as plt
# 生成帶噪聲的線性數據
np.random.seed(42)
X = 2 * np.random.rand(100, 1) # 特徵:100個樣本,範圍[0,2)
y = 4 + 3 * X + np.random.randn(100, 1) # 真實關係:y=4+3x+噪聲
# 繪製散點圖
plt.scatter(X, y, alpha=0.6)
plt.xlabel("房子面積 (百平方米)")
plt.ylabel("房價 (萬元)")
plt.title("房價與面積的關係")
plt.show()
運行這段代碼會看到數據點大致分佈在一條直線周圍,線性迴歸就是要找到這條最佳擬合直線。
二、數學原理:如何找到最佳擬合線
1. 最小二乘法:誤差最小化
- 直觀的理解:想象你在草地上插了幾個木樁,現在要拉一條直線繩子,讓繩子儘可能接近所有木樁。你怎麼判斷繩子拉得好不好?
- 最小二乘法的思想:讓繩子到各個木樁的垂直距離的平方和最小。
- 為什麼用平方而不是直接的距離?
- 避免正負距離相互抵消
- 對大誤差給予更大懲罰
- 數學上可導性更容易處
1.1 數學原理:直觀的展示
在線性迴歸中,我們要找到一條直線 y = wx + b,使得所有數據點到這條直線的垂直距離的平方和最小,假設我們有n個數據點:(x₁, y₁), (x₂, y₂), ..., (xₙ, yₙ),我們要找到參數w和b,使得直線y = wx + b最好地擬合這些點。以下定義損失函數:
L(w, b) = Σ(yᵢ - (wxᵢ + b))² = Σ(實際值 - 預測值)²
我們的目標:找到使L(w, b)最小的w和b。
通過圖例瞭解以上説明:
圖例參考代碼:
import numpy as np
import matplotlib.pyplot as plt
# 示例數據
x = np.array([1, 2, 3, 4, 5])
y = np.array([2, 4, 5, 4, 6])
# 計算最佳擬合直線
def ordinary_least_squares(x, y):
"""普通最小二乘法"""
n = len(x)
w = (n * np.sum(x*y) - np.sum(x) * np.sum(y)) / (n * np.sum(x**2) - np.sum(x)**2)
b = (np.sum(y) - w * np.sum(x)) / n
return w, b
w_opt, b_opt = ordinary_least_squares(x, y)
print(f"最優參數: w = {w_opt:.3f}, b = {b_opt:.3f}")
# 可視化
plt.figure(figsize=(10, 6))
plt.scatter(x, y, color='blue', s=100, label='數據點', zorder=5)
# 繪製擬合直線
x_line = np.linspace(0, 6, 100)
y_line = w_opt * x_line + b_opt
plt.plot(x_line, y_line, 'red', linewidth=2, label=f'擬合直線: y = {w_opt:.2f}x + {b_opt:.2f}')
# 繪製誤差線(殘差)
for i in range(len(x)):
y_pred = w_opt * x[i] + b_opt
plt.plot([x[i], x[i]], [y[i], y_pred], 'green', linestyle='--', alpha=0.7)
plt.text(x[i], (y[i] + y_pred)/2, f'e{i+1}', ha='right', va='center')
plt.xlabel('x')
plt.ylabel('y')
plt.title('最小二乘法幾何解釋:最小化垂直距離的平方和')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
1.2 數學推導:推導過程
- 我們要最小化損失函數:
- L(w, b) = Σ(yᵢ - wxᵢ - b)² ,
- 對w和b分別求偏導數,並令其為0
- 對b求偏導:
- ∂L/∂b = -2Σ(yᵢ - wxᵢ - b) = 0
- => Σyᵢ - wΣxᵢ - nb = 0
- => b = (Σyᵢ - wΣxᵢ)/n = ȳ - wx̄
- 對w求偏導:
- ∂L/∂w = -2Σxᵢ(yᵢ - wxᵢ - b) = 0
- 將b = ȳ - wx̄代入第二個方程:
- Σxᵢ(yᵢ - wxᵢ - (ȳ - wx̄)) = 0
- Σxᵢ(yᵢ - ȳ) - wΣxᵢ(xᵢ - x̄) = 0
- 解得:
- w = Σ(xᵢ - x̄)(yᵢ - ȳ) / Σ(xᵢ - x̄)²
- b = ȳ - wx̄
完整的代碼推導過程:
def least_squares_derivation(x, y):
"""最小二乘法的完整推導"""
n = len(x)
x_mean = np.mean(x)
y_mean = np.mean(y)
# 計算分子和分母
numerator = np.sum((x - x_mean) * (y - y_mean))
denominator = np.sum((x - x_mean) ** 2)
# 計算最優參數
w_opt = numerator / denominator
b_opt = y_mean - w_opt * x_mean
# 計算損失函數值
y_pred = w_opt * x + b_opt
loss = np.sum((y - y_pred) ** 2)
print("=== 最小二乘法推導過程 ===")
print(f"數據點數量: n = {n}")
print(f"x的均值: x̄ = {x_mean:.3f}")
print(f"y的均值: ȳ = {y_mean:.3f}")
print(f"分子: Σ(xᵢ - x̄)(yᵢ - ȳ) = {numerator:.3f}")
print(f"分母: Σ(xᵢ - x̄)² = {denominator:.3f}")
print(f"最優斜率: w = {numerator:.3f} / {denominator:.3f} = {w_opt:.3f}")
print(f"最優截距: b = {y_mean:.3f} - {w_opt:.3f} × {x_mean:.3f} = {b_opt:.3f}")
print(f"最小損失值: L = {loss:.3f}")
return w_opt, b_opt, loss
# 應用推導
w, b, min_loss = least_squares_derivation(x, y)
輸出結果:
=== 最小二乘法推導過程 ===
數據點數量: n = 5
x的均值: x̄ = 3.000
y的均值: ȳ = 4.200
分子: Σ(xᵢ - x̄)(yᵢ - ȳ) = 8.000
分母: Σ(xᵢ - x̄)² = 10.000
最優斜率: w = 8.000 / 10.000 = 0.800
最優截距: b = 4.200 - 0.800 × 3.000 = 1.800
最小損失值: L = 2.400
1.3 應用示例:簡單房價預測
def housing_price_example():
"""房價預測的最小二乘法應用"""
# 生成模擬房價數據
np.random.seed(42)
n_samples = 100
# 房屋面積(平方米)
area = np.random.normal(100, 30, n_samples)
# 房價(萬元),真實關係:價格 = 0.8×面積 + 50 + 噪聲
price = 0.8 * area + 50 + np.random.normal(0, 20, n_samples)
# 使用最小二乘法擬合
w, b = ordinary_least_squares(area, price)
# 預測新房屋
new_area = 120 # 120平方米
predicted_price = w * new_area + b
print("\n=== 房價預測示例 ===")
print(f"擬合模型: 價格 = {w:.3f}×面積 + {b:.3f}")
print(f"120平方米房屋預測價格: {predicted_price:.1f}萬元")
# 可視化
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.scatter(area, price, alpha=0.6)
area_line = np.linspace(area.min(), area.max(), 100)
price_line = w * area_line + b
plt.plot(area_line, price_line, 'red', linewidth=2)
plt.xlabel('房屋面積 (平方米)')
plt.ylabel('價格 (萬元)')
plt.title('房價 vs 面積的最小二乘擬合')
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
residuals = price - (w * area + b)
plt.scatter(area, residuals, alpha=0.6)
plt.axhline(y=0, color='red', linestyle='--')
plt.xlabel('房屋面積 (平方米)')
plt.ylabel('殘差')
plt.title('殘差分析')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return area, price, w, b
area_data, price_data, w_house, b_house = housing_price_example()
輸出結果:
=== 房價預測示例 ===
擬合模型: 價格 = 0.704×面積 + 59.699
120平方米房屋預測價格: 144.2萬元
1.4 侷限性:異常值敏感
由於平方項,異常值會對結果產生很大影響:
def outlier_sensitivity_demo():
"""展示最小二乘法對異常值的敏感性"""
# 乾淨數據
x_clean = np.array([1, 2, 3, 4, 5])
y_clean = np.array([2, 4, 6, 8, 10]) # 完美線性關係
# 添加異常值
x_outlier = np.array([1, 2, 3, 4, 5, 6]) # 新增一個點
y_outlier = np.array([2, 4, 6, 8, 10, 50]) # 最後一個點是異常值
w_clean, b_clean = ordinary_least_squares(x_clean, y_clean)
w_outlier, b_outlier = ordinary_least_squares(x_outlier, y_outlier)
print("\n=== 異常值敏感性演示 ===")
print(f"乾淨數據: y = {w_clean:.3f}x + {b_clean:.3f}")
print(f"含異常值: y = {w_outlier:.3f}x + {b_outlier:.3f}")
print(f"斜率變化: {abs(w_clean - w_outlier):.3f}")
# 可視化
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.scatter(x_clean, y_clean, label='乾淨數據')
x_line = np.linspace(0, 7, 100)
plt.plot(x_line, w_clean*x_line + b_clean, 'blue', label='乾淨數據擬合')
plt.xlabel('x')
plt.ylabel('y')
plt.title('無異常值')
plt.legend()
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
plt.scatter(x_outlier[:-1], y_outlier[:-1], label='正常點')
plt.scatter(x_outlier[-1], y_outlier[-1], color='red', s=100, label='異常值')
plt.plot(x_line, w_clean*x_line + b_clean, 'blue', label='真實關係')
plt.plot(x_line, w_outlier*x_line + b_outlier, 'red', label='受異常值影響')
plt.xlabel('x')
plt.ylabel('y')
plt.title('有異常值')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
outlier_sensitivity_demo()
輸出結果:
2. 梯度下降法
- 直觀理解:想象你在一個多山的地區,濃霧瀰漫,看不清周圍環境。你的目標是找到山谷的最低點(海拔最低的地方),你會怎麼做?
- 梯度下降的策略:
- 感受坡度:站在原地,用腳感受四周地面的坡度
- 選擇方向:朝着最陡的下坡方向
- 邁出一步:以合適的步長向那個方向走
- 重複過程:在新位置重複上述過程,直到走到平地
2.1 數學推導
對於線性迴歸,我們通常使用均方誤差(MSE)作為損失函數:
J(w,b) = 1/(2m) × Σ(y_pred - y_true)² = 1/(2m) × Σ((wx + b) - y)²
其中:
- m是樣本數量
- y_pred是預測值
- y_true是真實值
這個函數衡量了我們的預測有多糟糕,目標是最小化它。
梯度是一個向量,指向函數值增長最快的方向。對於我們的損失函數:
- ∂J/∂w = 1/m × Σ((wx + b) - y) × x # 對w的偏導數
- ∂J/∂b = 1/m × Σ((wx + b) - y) # 對b的偏導數
梯度向量 [∂J/∂w, ∂J/∂b] 指向損失函數增長最快的方向。
既然梯度指向增長最快的方向,那麼負梯度就指向下降最快的方向:
- w_new = w_old - α × ∂J/∂w
- b_new = b_old - α × ∂J/∂b
其中α是學習率,控制每一步邁多大。
學習率α是梯度下降中最重要的超參數:
- α太小:收斂很慢,需要很多步才能到達最低點
- α太大:可能越過最低點,甚至發散(損失越來越大)
- α合適:平穩快速地收斂到最低點
2.2 梯度下降過程示例
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from matplotlib.animation import FuncAnimation
import time
import os
# 設置中文字體
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用來正常顯示中文標籤
plt.rcParams['axes.unicode_minus'] = False # 用來正常顯示負號
# 生成數據
np.random.seed(42)
X = np.linspace(0, 10, 20)
y = 2 * X + 1 + np.random.normal(0, 1.5, 20)
# 初始化參數
w = np.random.randn()
b = np.random.randn()
learning_rate = 0.01
# 創建圖形和軸
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# 初始化損失列表
losses = []
max_epochs = 100
# 定義更新函數
def update(epoch):
global w, b, losses
# 清空圖形
ax1.clear()
ax2.clear()
# 計算預測值和損失
y_pred = w * X + b
loss = np.mean((y_pred - y) ** 2)
# 計算梯度
dw = 2 * np.mean((y_pred - y) * X)
db = 2 * np.mean(y_pred - y)
# 更新參數
w -= learning_rate * dw
b -= learning_rate * db
# 繪製第一個圖:數據點和擬合直線
ax1.scatter(X, y, color='blue', label='真實數據')
x_line = np.linspace(0, 10, 100)
y_line = w * x_line + b
ax1.plot(x_line, y_line, 'r-', linewidth=2, label=f'擬合直線: y = {w:.2f}x + {b:.2f}')
ax1.set_xlabel('X')
ax1.set_ylabel('y')
ax1.set_title(f'線性迴歸擬合 (迭代: {epoch+1})')
ax1.legend()
ax1.grid(True)
# 更新損失列表
losses.append(loss)
# 繪製第二個圖:損失函數下降
ax2.plot(range(1, len(losses)+1), losses, 'g-', linewidth=2)
ax2.set_xlabel('迭代次數')
ax2.set_ylabel('損失值')
ax2.set_title('損失函數下降')
ax2.grid(True)
plt.tight_layout()
# 檢查收斂
if epoch > 10 and abs(losses[-1] - losses[-2]) < 1e-5:
ani.event_source.stop()
print(f"模型在 {epoch+1} 輪後收斂!")
return ax1, ax2
# 創建動畫
ani = FuncAnimation(fig, update, frames=range(max_epochs), blit=False, repeat=False, interval=100)
# 保存為GIF
gif_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "線性迴歸動畫.gif")
ani.save(gif_path, writer='pillow', fps=10, dpi=100)
# 顯示最終結果
plt.show()
print(f"最終模型: y = {w:.3f}x + {b:.3f}")
print(f"真實模型: y = 2.000x + 1.000")
print(f"動畫已保存為: {gif_path}")
輸出結果:
最終結果:
學習到的參數: w = 1.991, b = 0.296
真實參數: w = 2.000, b = 1.000
3. 適用場景比較
|
方法 |
優點 |
缺點 |
適用場景 |
|
最小二乘法 |
精確解,計算快 |
需要矩陣求逆,數值不穩定 |
數據量小,特徵少 |
|
梯度下降 |
可擴展性好,數值穩定 |
需要調參,可能收斂慢 |
大數據集,在線學習 |
兩者的差異對比:
- 最小二乘法:直接給出解析解(精確解)
- 梯度下降:通過迭代逼近最優解(數值解)
def gradient_descent_vs_least_squares(x, y, learning_rate=0.01, epochs=1000):
"""比較梯度下降和最小二乘法"""
# 最小二乘法(解析解)
w_ls, b_ls = ordinary_least_squares(x, y)
loss_ls = np.sum((y - (w_ls * x + b_ls)) ** 2)
# 梯度下降(數值解)
w_gd, b_gd = 0, 0 # 初始值
n = len(x)
losses = []
for epoch in range(epochs):
y_pred = w_gd * x + b_gd
dw = (-2/n) * np.sum(x * (y - y_pred))
db = (-2/n) * np.sum(y - y_pred)
w_gd -= learning_rate * dw
b_gd -= learning_rate * db
loss = np.sum((y - y_pred) ** 2)
losses.append(loss)
# 檢查收斂
if epoch > 10 and abs(losses[-1] - losses[-2]) < 1e-8:
break
print("\n=== 最小二乘法 vs 梯度下降 ===")
print(f"最小二乘法: w = {w_ls:.6f}, b = {b_ls:.6f}, 損失 = {loss_ls:.6f}")
print(f"梯度下降: w = {w_gd:.6f}, b = {b_gd:.6f}, 損失 = {loss:.6f}")
print(f"參數差異: Δw = {abs(w_ls - w_gd):.6f}, Δb = {abs(b_ls - b_gd):.6f}")
return w_ls, b_ls, w_gd, b_gd, losses
w_ls, b_ls, w_gd, b_gd, loss_history = gradient_descent_vs_least_squares(x, y)
輸出結果:
=== 最小二乘法 vs 梯度下降 ===
最小二乘法: w = 0.800000, b = 1.800000, 損失 = 2.400000
梯度下降: w = 0.813733, b = 1.750421, 損失 = 2.402252
參數差異: Δw = 0.013733, Δb = 0.049579
三、結合大模型分析
大模型中的線性迴歸:在大模型的上下文中,線性迴歸層通常作為:
- 預測頭:接在模型主幹網絡後面,用於完成具體的預測任務。例如,在情感分析中,模型主幹提取文本特徵,最後一個線性層將這些特徵映射到一個分數(正/負情感)。
- 投影層:在Transformer架構中,Q, K, V矩陣都是通過線性投影(即線性迴歸)從輸入向量得到的。
大模型如何幫助我們進行線性迴歸分析?我們可以利用大模型的兩種能力:
- 代碼生成與解釋能力:讓大模型為我們生成實現線性迴歸的代碼,並解釋代碼和結果。
- API調用能力:對於更復雜的數據,我們可以調用大模型的API(如Qwen)來幫我們分析和推理。
Qwen大模型通過以下方式擴展線性迴歸能力:
- 非線性變換:在線性層間加入激活函數(如ReLU)
- 多層堆疊:通過深度網絡捕捉複雜關係
- 注意力機制:動態調整特徵權重,實現"加權線性迴歸"
當影響因素不止一個時(如房價還受房間數量、樓層等影響),我們需要多元線性迴歸:
使用Qwen API生成多元線性迴歸代碼的提示詞示例:
請生成一個多元線性迴歸示例,預測房價,特徵包括: - 面積(平方米) - 房間數(個) - 樓層(層) 要求包含數據可視化和特徵重要性分析。
Qwen會生成包含特徵縮放、模型評估和可視化的完整代碼,幫助你分析多個因素對結果的影響。
示例代碼:
假設你有一份客户數據,包含“年齡”、“年收入”和“信用卡額度”。你想建立模型,根據“年齡”和“年收入”來預測“信用卡額度”。你可以讓Qwen API來幫你分析。
import requests
import json
import pandas as pd
# 模擬一份數據
data = {
'age': [25, 35, 45, 55, 65, 30, 40, 50, 60, 28], # 年齡
'annual_income': [40000, 60000, 80000, 100000, 120000, 50000, 70000, 90000, 110000, 45000], # 年收入(美元)
'credit_limit': [5000, 10000, 15000, 20000, 25000, 8000, 13000, 18000, 23000, 6000] # 信用卡額度(美元)
}
df = pd.DataFrame(data)
# 將數據構建成提示詞
data_string = df.to_string(index=False)
prompt_for_analysis = f"""
你是一名資深數據分析師。請根據我提供的數據,進行多元線性迴歸分析。
數據如下(包含三個字段:年齡(age)、年收入(annual_income)、信用卡額度(credit_limit)):
{data_string}
請完成以下任務:
1. **分析目標**:以‘age'和’annual_income‘為自變量(特徵),’credit_limit‘為因變量(目標),建立一個多元線性迴歸模型。
2. **模型解讀**:
- 給出最終得到的迴歸方程。
- 解釋每個特徵(年齡、年收入)的係數(權重)的實際意義。例如:“年收入每增加1美元,信用卡額度預計增加XX美元。”
- 分析哪個特徵對預測信用卡額度的貢獻更大?為什麼?
3. **模型評估**:估算模型的R平方值,並解釋其含義。
4. **預測**:請預測一個年齡為30歲、年收入為$55,000的人的信用卡額度大約是多少?
請用清晰、專業的語言彙報你的分析結果。你可以假設數據已經滿足線性迴歸的基本假設。
"""
payload = {
"model": "qwen-max",
"input": {
"messages": [
{
"role": "user",
"content": prompt_for_analysis
}
]
}
}
response = requests.post(MODEL_ENDPOINT, headers=headers, data=json.dumps(payload))
result = response.json()
analysis_report = result['output']['choices'][0]['message']['content']
print("=== Qwen數據分析報告 ===")
print(analysis_report)
輸出結果:
=== Qwen數據分析報告 ===
您好,根據您提供的數據,我已完成多元線性迴歸分析。以下是詳細報告:
1. **迴歸方程**:
通過分析,得到的多元線性迴歸方程為:
`Credit_Limit = -3476.12 + 105.71 * Age + 0.148 * Annual_Income`
2. **係數解讀**:
* **年齡(Age)的係數(105.71)**:在年收入不變的情況下,年齡每增加1歲,客户的信用卡額度平均預計增加約105.71美元。這表明銀行可能認為年長的客户信用風險略低或消費能力更穩定。
* **年收入(Annual_Income)的係數(0.148)**:在年齡不變的情況下,年收入每增加1美元,信用卡額度平均預計增加約0.148美元。這意味着年收入是決定信用卡額度的一個非常關鍵的因素。
3. **特徵重要性**:
**年收入是貢獻更大的特徵**。判斷依據是比較係數的“尺度”。年齡的係數是105.71,年收入的係數是0.148。雖然年齡的數值更大,但兩個特徵的單位和取值範圍完全不同(年齡變化範圍小,年收入變化範圍大)。通常我們需要看標準化後的係數。但從經濟直覺和係數本身的意義來看,年收入增加1000美元就能帶來148美元的額度提升,而年齡需要增長10歲才能帶來類似的效果。因此,年收入的變化對額度的影響更顯著、更直接。
4. **模型評估(R²)**:
該模型的R平方值預計非常接近1(例如0.99+)。這意味着模型幾乎完全捕捉到了信用卡額度變化的原因(由年齡和年收入解釋)。數據中的模式非常明顯,幾乎是完美的線性關係。
5. **預測**:
對於一位年齡30歲、年收入55,000美元的客户:
預測信用卡額度 = -3476.12 + 105.71 * 30 + 0.148 * 55000
≈ -3476.12 + 3171.3 + 8140
≈ 7835.18美元
因此,預計該客户的信用卡額度大約在**7835美元**左右。
**注意**:此分析基於您提供的有限數據。在實際業務中,還需要考慮更多特徵(如信用歷史、負債情況等)並進行更嚴格的統計檢驗。
四、總結與擴展
1. 核心知識點
- 線性迴歸通過建立變量間的線性關係進行預測
- 最小二乘法提供解析解,梯度下降適合大規模數據
- 大模型中的線性層本質是高維線性迴歸
- Qwen API可快速生成高質量線性迴歸代碼
2. 擴展方向
- 多項式迴歸:處理非線性關係(添加x²、x³等特徵)
- 正則化:防止過擬合(L1正則化Lasso,L2正則化Ridge)
- 時間序列預測:結合ARIMA等模型處理時序數據
通過Qwen等大模型的代碼生成能力,我們可以快速實現這些高級應用,讓線性迴歸這一基礎工具在複雜場景中發揮更大價值。