新手入门必读的人工智能算法-监督学习算法中的线性回归算法
监督学习的灵魂在于“从已知历史答案中学习规律,以预测未来”。 它是一切预测性AI应用的基础。理解监督学习,是踏入机器学习乃至人工智能世界最关键的第一步。
一、核心思想
监督学习的核心思想是利用一组已知输入和对应输出的样本数据来训练模型,让模型学习从输入到输出的映射关系,从而能够对新的、未知的输入做出准确的预测或判断。
一个形象的比喻:
老师(训练数据) 在教学生(模型) 认动物。老师会展示很多图片(特征),并告诉学生每张图片是什么动物(标签,即正确答案)。学生通过反复观察和记忆,总结出规律(例如:“有长脖子和斑点的是长颈鹿”)。之后,当老师拿出一张学生没见过的动物图片时,学生就能根据学到的规律判断出这是什么动物。
二、关键要素
-
标签: 这是“监督”的来源,即每个训练样本的正确答案或目标值。它是模型要预测的东西。
-
例子: 在垃圾邮件分类中,“垃圾邮件”或“正常邮件”就是标签。
-
-
特征: 用来描述每个样本的属性或变量。它们是模型的输入。
-
例子: 对于一封邮件,特征可以是“发件人地址是否在通讯录”、“邮件主题是否包含‘免费’、‘获奖’等关键词”、“邮件正文长度”等。
-
-
模型: 一个数学函数或算法,它接收特征作为输入,并输出一个预测值。训练的目的就是找到这个函数的最佳参数。
-
训练数据: 一组已经标注好的
(特征, 标签)对,用于“教导”模型。 -
测试数据: 另一组模型从未见过的标注数据,用于独立评估模型的泛化能力(即处理新数据的能力)。
三、主要任务类型
根据输出变量(标签)的类型,监督学习可以分为两大类:
1. 回归
-
目标: 预测一个连续的数值。
-
本质: 找到一个函数,能够最好地拟合数据点。
-
经典问题:
-
根据房屋面积、地段、房龄预测房价。
-
根据历史销量预测未来销售额。
-
根据患者各项指标预测其康复天数。
-
-
核心算法: 线性回归、多项式回归、支持向量回归、决策树回归等。
2. 分类
-
目标: 预测一个离散的类别或标签。
-
本质: 在特征空间中找到一个决策边界,将不同类别的数据点分开。
-
经典问题:
-
判断一封邮件是否为垃圾邮件(二分类:是/否)。
-
识别图片中的动物是什么(多分类:猫/狗/鸟...)。
-
根据肿瘤特征判断其是良性还是恶性。
-
-
核心算法: 逻辑回归、支持向量机、决策树/随机森林、K-近邻、朴素贝叶斯、神经网络等。
四、标准工作流程
-
数据收集与标注: 获取原始数据并为其打上正确的标签。这是最耗时、成本最高的步骤之一。
-
数据预处理与特征工程:
-
清洗: 处理缺失值、异常值。
-
转换: 对类别型特征进行编码(如独热编码)。
-
缩放: 标准化或归一化,使不同尺度的特征具有可比性。
-
特征工程: 创造新的、更有信息量的特征。这是提升模型性能的关键。
-
-
划分数据集: 通常按一定比例(如7:3或8:2)随机划分为训练集、验证集和测试集。
-
训练集: 用于训练模型。
-
验证集: 用于在训练过程中调整模型超参数、选择模型。
-
测试集: 用于最终、一次性地评估模型性能,绝不能用于训练或调参。
-
-
选择模型与训练: 根据问题类型和数据特点,选择一个或多个算法,用训练集数据拟合模型。
-
模型评估:
-
回归常用指标: 均方误差、平均绝对误差、R²分数。
-
分类常用指标: 准确率、精确率、召回率、F1分数、ROC-AUC。
-
在验证集上进行评估,以指导模型选择和改进。
-
-
调优与验证: 调整模型的超参数(如决策树的深度、随机森林中树的数量),在验证集上反复验证,寻找最佳组合。
-
最终测试与部署: 使用从未参与过任何训练的测试集,对优化后的最终模型进行一次性评估,得到其泛化性能的可靠估计。若性能达标,则将模型部署到实际应用中。
五、典型应用场景
-
计算机视觉: 图像分类(识别猫狗)、目标检测(自动驾驶中识别行人车辆)、人脸识别。
-
自然语言处理: 文本分类(情感分析、新闻分类)、机器翻译、垃圾邮件过滤、语音识别。
-
医疗健康: 疾病诊断(基于医学影像)、药物发现、住院风险预测。
-
金融风控: 信用评分、欺诈交易检测、股票价格预测。
-
推荐系统: 电商商品推荐、视频内容推荐、音乐推荐。
六、核心算法介绍
1. 线性回归算法
核心思想: 假设目标值 y 和特征 X 之间存在线性关系,即通过数据拟合一条直线(二维)、一个平面(三维)或一个超平面(高维)。

优点:
-
简单直观,易于理解和实现。
-
计算效率高,训练和预测速度非常快。
-
当特征与目标确实存在线性关系时,效果很好。
-
模型参数(权重)具有可解释性,可以直接看出每个特征对目标的影响方向和大小。
缺点:
-
对非线性关系建模能力差。如果数据关系不是线性的,强行拟合会导致效果很差。
-
对异常值和噪声数据敏感。
-
前提假设强:要求特征间相互独立(无多重共线性)、误差项服从正态分布等,现实数据往往难以完美满足。
典型应用: 房价预测(基于面积、房间数)、销量预测、经济趋势分析等假设为线性关系的场景。
线性回归算法应用实例:
房价预测例子
一、背景与目标
根据房屋的各种特征预测其市场价格,帮助买家、卖家、银行等做出更明智的房产决策
-
主要目标:构建准确预测房价的模型
-
次要目标:
-
理解影响房价的关键因素
-
量化各特征对房价的影响程度
-
提供模型解释和预测区间
-
二、数据准备与探索
代码环境配置
# 环境配置详细说明
import sys
import platform
print("=== 环境信息 ===")
print(f"Python版本: {sys.version}")
print(f"操作系统: {platform.platform()}")
# 检查并安装必要库
required_packages = {
'numpy': '1.21.0',
'pandas': '1.3.0',
'matplotlib': '3.4.0',
'seaborn': '0.11.0',
'scikit-learn': '0.24.0',
'statsmodels': '0.12.0',
'scipy': '1.7.0'
}
def check_install_packages():
"""
检查并安装必要的包
"""
import importlib
import subprocess
for package, min_version in required_packages.items():
try:
mod = importlib.import_module(package)
version = getattr(mod, '__version__', 'unknown')
print(f"✓ {package}: {version}")
except ImportError:
print(f"✗ {package}: 未安装,正在安装...")
subprocess.check_call([sys.executable, "-m", "pip", "install",
f"{package}>={min_version}"])
print(f" {package}: 安装完成")
check_install_packages()
1. 获取或生成数据
(1)如果有真实数据:加载CSV、Excel或数据库中的房产数据
猜测房价与其影响因子呈现线性相关关系,对假设进行检验,对数据预处理后进行线性回归假设检验 ,下面是检验方法:
线性关系检验
# 详细线性关系检验代码
def check_linear_assumption(X, y, feature_names):
"""
检验线性关系假设
"""
from scipy import stats
n_features = X.shape[1]
fig, axes = plt.subplots(1, n_features, figsize=(5*n_features, 4))
for i in range(n_features):
# 绘制散点图和拟合线
axes[i].scatter(X[:, i], y, alpha=0.6, s=20)
# 计算Pearson相关系数
corr, p_value = stats.pearsonr(X[:, i], y)
# 添加线性回归线
z = np.polyfit(X[:, i], y, 1)
p = np.poly1d(z)
axes[i].plot(X[:, i], p(X[:, i]), "r--", alpha=0.8)
axes[i].set_xlabel(feature_names[i])
axes[i].set_ylabel('房价')
axes[i].set_title(f'{feature_names[i]}\n相关系数: {corr:.3f}\np值: {p_value:.4f}')
axes[i].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 输出相关性分析
print("=== 特征与目标变量相关性分析 ===")
for i, name in enumerate(feature_names):
corr, p_value = stats.pearsonr(X[:, i], y)
print(f"{name}: 相关系数={corr:.4f}, p值={p_value:.4f}")
多重共线性诊断
def check_multicollinearity(X, feature_names):
"""
检查多重共线性
"""
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant
# 计算方差膨胀因子(VIF)
X_with_const = add_constant(X)
vif_data = pd.DataFrame()
vif_data["特征"] = ['截距'] + list(feature_names)
vif_data["VIF"] = [variance_inflation_factor(X_with_const, i)
for i in range(X_with_const.shape[1])]
print("=== 多重共线性诊断 ===")
print(vif_data)
print("\nVIF解释:")
print("VIF < 5: 无多重共线性")
print("5 <= VIF < 10: 中度多重共线性")
print("VIF >= 10: 严重多重共线性")
return vif_data
(2)如果没有真实数据:使用Python生成模拟数据
数据生成原理
# 更真实的数据生成过程
def generate_housing_data(n_samples=1000, seed=42):
"""
生成更真实的房价数据
"""
np.random.seed(seed)
# 1. 基础特征
# 面积:正偏态分布(大多数房子面积适中,少数很大)
area = np.random.gamma(shape=2.5, scale=40, size=n_samples) + 60
# 卧室数:离散分布
bedrooms = np.random.choice([1, 2, 3, 4, 5], size=n_samples, p=[0.1, 0.25, 0.35, 0.2, 0.1])
# 卫生间数:与卧室数相关
bathrooms = np.round(bedrooms * np.random.uniform(0.8, 1.2, n_samples))
bathrooms = np.clip(bathrooms, 1, 4).astype(int)
# 楼层数
floors = np.random.choice([1, 2, 3], size=n_samples, p=[0.5, 0.4, 0.1])
# 房龄:指数分布
age = np.random.exponential(20, n_samples)
age = np.where(age > 50, 50, age) # 截断
# 位置特征
# 到市中心的距离
distance = np.random.beta(a=2, b=5, size=n_samples) * 30 # 大多数房子不在市中心
# 学区评分(1-10)
school_rating = np.random.normal(6, 1.5, n_samples)
school_rating = np.clip(school_rating, 1, 10)
# 2. 交互效应和复杂关系
# 价格基础公式(更真实的非线性关系)
base_price = (
8000 * area + # 面积影响
50000 * bedrooms + # 卧室影响
30000 * bathrooms + # 卫生间影响
20000 * floors - # 楼层影响
2000 * age - # 房龄折旧
3000 * distance**0.7 + # 距离影响(非线性)
15000 * school_rating + # 学区影响
50000 * (area > 200).astype(int) # 大户型溢价
)
# 3. 添加噪声(异方差性:价格越高,波动越大)
noise = np.random.normal(0, base_price * 0.1, n_samples) # 10%的噪声
# 最终价格
price = base_price + noise
# 4. 创建DataFrame
data = pd.DataFrame({
'面积': area,
'卧室数': bedrooms,
'卫生间数': bathrooms,
'楼层数': floors,
'房龄': age,
'距离市中心': distance,
'学区评分': school_rating,
'房价': price
})
return data
# 生成数据
data = generate_housing_data(n_samples=1000)
print(f"生成数据形状: {data.shape}")
print("\n数据描述:")
print(data.describe())
# 添加新特征:每平方米价格
data['每平米价格'] = data['房价'] / data['面积']
print("\n=== 特征工程示例 ===")
# 1. 创建交互特征
data['面积卧室比'] = data['面积'] / data['卧室数']
data['卫生间卧室比'] = data['卫生间数'] / data['卧室数']
# 2. 创建多项式特征
data['面积平方'] = data['面积'] ** 2
data['距离平方'] = data['距离市中心'] ** 2
# 3. 分箱处理(将连续变量转为分类变量)
data['房龄分段'] = pd.cut(data['房龄'],
bins=[0, 5, 10, 20, 50],
labels=['新房', '次新房', '旧房', '老房'])
data['面积分段'] = pd.cut(data['面积'],
bins=[0, 80, 120, 180, 500],
labels=['小户型', '中户型', '大户型', '豪宅'])
print(data[['面积', '卧室数', '房龄分段', '面积分段', '房价']].head())
三、数据探索(EDA)
必须做的检查:
-
查看数据基本信息
data.shape # 查看数据维度 data.head() # 查看前几行 data.describe() # 查看统计摘要 data.info() # 查看数据类型和缺失值 -
可视化分析
-
绘制每个特征与房价的散点图
-
绘制特征之间的相关性热力图
-
检查房价的分布(直方图、箱线图)
-
异常值检测
-
使用箱线图或IQR方法识别异常值
-
-
-
决定是否处理(删除、转换或保留)异常值
def comprehensive_eda(data, target_col='房价'): """ 全面的探索性数据分析 """ print("=== 探索性数据分析 ===") # 1. 基本统计信息 print("\n1. 基本统计信息:") print(data.describe()) # 2. 数据类型和缺失值 print("\n2. 数据类型和缺失值:") print(data.info()) print(f"\n缺失值统计:") print(data.isnull().sum()) # 3. 目标变量分布 fig, axes = plt.subplots(2, 3, figsize=(15, 10)) # 房价分布 axes[0, 0].hist(data[target_col], bins=50, edgecolor='black', alpha=0.7) axes[0, 0].set_xlabel('房价') axes[0, 0].set_ylabel('频率') axes[0, 0].set_title('房价分布') axes[0, 0].grid(True, alpha=0.3) # 房价箱线图 axes[0, 1].boxplot(data[target_col]) axes[0, 1].set_ylabel('房价') axes[0, 1].set_title('房价箱线图') axes[0, 1].grid(True, alpha=0.3) # 房价Q-Q图 from scipy import stats stats.probplot(data[target_col], dist="norm", plot=axes[0, 2]) axes[0, 2].set_title('房价Q-Q图') axes[0, 2].grid(True, alpha=0.3) # 4. 特征分布 numeric_cols = data.select_dtypes(include=[np.number]).columns numeric_cols = numeric_cols.drop(target_col) if target_col in numeric_cols else numeric_cols for i, col in enumerate(numeric_cols[:3]): # 只显示前3个 axes[1, i].hist(data[col], bins=30, edgecolor='black', alpha=0.7) axes[1, i].set_xlabel(col) axes[1, i].set_ylabel('频率') axes[1, i].set_title(f'{col}分布') axes[1, i].grid(True, alpha=0.3) plt.tight_layout() plt.show() # 5. 相关性分析 print("\n3. 相关性分析:") corr_matrix = data[numeric_cols].corr() plt.figure(figsize=(12, 10)) sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', square=True, cbar_kws={"shrink": .8}) plt.title('特征相关性热力图') plt.tight_layout() plt.show() # 6. 异常值检测 print("\n4. 异常值检测 (基于IQR):") Q1 = data[numeric_cols].quantile(0.25) Q3 = data[numeric_cols].quantile(0.75) IQR = Q3 - Q1 outliers = ((data[numeric_cols] < (Q1 - 1.5 * IQR)) | (data[numeric_cols] > (Q3 + 1.5 * IQR))).sum() outlier_summary = pd.DataFrame({ '特征': outliers.index, '异常值数量': outliers.values, '异常值比例 (%)': (outliers.values / len(data) * 100).round(2) }) print(outlier_summary) # 执行EDA comprehensive_eda(data)
四、数据预处理
1. 处理缺失值
处理方法:
-
数值特征:用均值、中位数或众数填充
-
分类特征:用众数填充或创建新类别
2. 处理异常值
策略选择:
-
保留:如果异常值是真实数据且数量少
-
删除:如果异常值明显是错误
-
转换:使用对数变换或缩尾处理
3. 特征工程
可创建的新特征:
-
交互特征:面积/卧室数(每卧室面积)
-
多项式特征:面积²、房龄²
-
分箱:将连续年龄分为"新房"、"旧房"等类别
4. 特征编码
处理分类特征:
-
有序分类:使用标签编码(0,1,2,...)
-
无序分类:使用独热编码(创建哑变量)
5. 特征缩放
标准化或归一化:
-
线性回归通常不需要,但标准化可加速收敛
-
使用StandardScaler或MinMaxScaler
6. 划分数据集
标准做法:
-
训练集:80%(用于训练模型)
-
测试集:20%(用于评估模型)
-
使用
train_test_split函数
详细代码
def comprehensive_preprocessing(data, target_col='房价', test_size=0.2, random_state=42):
"""
全面的数据预处理流程
"""
print("=== 数据预处理 ===")
# 1. 分离特征和目标变量
X = data.drop(columns=[target_col])
y = data[target_col]
# 2. 处理缺失值(如果存在)
if X.isnull().sum().sum() > 0:
print("处理缺失值...")
# 数值特征用中位数填充
numeric_cols = X.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
if X[col].isnull().sum() > 0:
median_val = X[col].median()
X[col] = X[col].fillna(median_val)
print(f" {col}: 用中位数 {median_val:.2f} 填充")
# 分类特征用众数填充
categorical_cols = X.select_dtypes(include=['object', 'category']).columns
for col in categorical_cols:
if X[col].isnull().sum() > 0:
mode_val = X[col].mode()[0]
X[col] = X[col].fillna(mode_val)
print(f" {col}: 用众数 '{mode_val}' 填充")
# 3. 分离数值特征和分类特征
numeric_features = X.select_dtypes(include=[np.number]).columns.tolist()
categorical_features = X.select_dtypes(include=['object', 'category']).columns.tolist()
print(f"\n数值特征: {numeric_features}")
print(f"分类特征: {categorical_features}")
# 4. 处理分类特征(编码)
if categorical_features:
print("\n处理分类特征...")
# 独热编码
X_encoded = pd.get_dummies(X[categorical_features], prefix=categorical_features, drop_first=True)
# 合并数值特征和编码后的分类特征
X_numeric = X[numeric_features]
X_processed = pd.concat([X_numeric, X_encoded], axis=1)
else:
X_processed = X[numeric_features].copy()
# 5. 特征缩放
print("\n特征缩放...")
from sklearn.preprocessing import StandardScaler, RobustScaler
# 对于有异常值的特征,使用RobustScaler
scaler_numeric = RobustScaler()
X_scaled_numeric = scaler_numeric.fit_transform(X_processed[numeric_features])
# 创建新的DataFrame
X_scaled = pd.DataFrame(X_scaled_numeric, columns=numeric_features, index=X_processed.index)
# 添加编码后的分类特征
if categorical_features:
for col in X_encoded.columns:
X_scaled[col] = X_encoded[col].values
# 6. 划分训练集和测试集
print(f"\n划分数据集 (测试集比例: {test_size})")
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=test_size, random_state=random_state
)
print(f"训练集: {X_train.shape[0]} 个样本, {X_train.shape[1]} 个特征")
print(f"测试集: {X_test.shape[0]} 个样本")
# 7. 保存预处理对象(用于新数据)
preprocessing_info = {
'numeric_features': numeric_features,
'categorical_features': categorical_features,
'scaler': scaler_numeric,
'feature_names': X_scaled.columns.tolist()
}
return X_train, X_test, y_train, y_test, preprocessing_info
# 执行预处理
X_train, X_test, y_train, y_test, preprocess_info = comprehensive_preprocessing(data)
五、模型训练
1. 选择算法
使用线性回归的原因:
-
简单易懂,适合初学者
-
计算效率高
-
可解释性强
-
作为基准模型
2. 模型训练步骤
使用scikit-learn:
from sklearn.linear_model import LinearRegression
# 创建模型实例
model = LinearRegression()
# 训练模型
model.fit(X_train, y_train)
# 查看模型参数
print(f"截距: {model.intercept_:.2f}")
print(f"系数: {model.coef_}")
3. 从零实现(理解原理)
手动实现梯度下降:
class SimpleLinearRegression:
def __init__(self, learning_rate=0.01, n_iterations=1000):
self.learning_rate = learning_rate
self.n_iterations = n_iterations
self.weights = None
self.bias = None
def fit(self, X, y):
n_samples, n_features = X.shape
self.weights = np.zeros(n_features)
self.bias = 0
for _ in range(self.n_iterations):
# 预测
y_pred = np.dot(X, self.weights) + self.bias
# 计算梯度
dw = (2/n_samples) * np.dot(X.T, (y_pred - y))
db = (2/n_samples) * np.sum(y_pred - y)
# 更新参数
self.weights -= self.learning_rate * dw
self.bias -= self.learning_rate * db
def predict(self, X):
return np.dot(X, self.weights) + self.bias
完整代码:
class DetailedLinearRegression:
"""
详细实现的线性回归类,包含完整的数学推导
"""
def __init__(self, method='ols', learning_rate=0.01, n_iterations=1000,
tolerance=1e-6, add_intercept=True):
"""
初始化线性回归模型
参数:
method: 求解方法 ('ols': 最小二乘法, 'gradient': 梯度下降)
learning_rate: 学习率(梯度下降使用)
n_iterations: 最大迭代次数(梯度下降使用)
tolerance: 收敛阈值(梯度下降使用)
add_intercept: 是否添加截距项
"""
self.method = method
self.learning_rate = learning_rate
self.n_iterations = n_iterations
self.tolerance = tolerance
self.add_intercept = add_intercept
self.theta = None # 参数向量(包括截距)
self.loss_history = [] # 损失历史
self.r_squared = None # R²分数
self.standard_errors = None # 标准误差
self.p_values = None # p值
self.confidence_intervals = None # 置信区间
def _add_intercept(self, X):
"""添加截距项(全为1的列)"""
intercept = np.ones((X.shape[0], 1))
return np.concatenate((intercept, X), axis=1)
def fit_ols(self, X, y):
"""
使用最小二乘法求解
数学原理:
θ = (XᵀX)⁻¹Xᵀy
"""
print("使用最小二乘法求解...")
# 1. 添加截距项
if self.add_intercept:
X_with_intercept = self._add_intercept(X)
else:
X_with_intercept = X
# 2. 计算解析解
# 注意:使用伪逆避免矩阵不可逆的问题
X_T = X_with_intercept.T
X_T_X = X_T @ X_with_intercept
try:
# 尝试直接求逆
X_T_X_inv = np.linalg.inv(X_T_X)
except np.linalg.LinAlgError:
print("矩阵不可逆,使用伪逆...")
X_T_X_inv = np.linalg.pinv(X_T_X)
self.theta = X_T_X_inv @ X_T @ y
# 3. 计算统计量
self._calculate_statistics(X_with_intercept, y)
def fit_gradient_descent(self, X, y):
"""
使用梯度下降法求解
数学原理:
损失函数: J(θ) = 1/(2m) * Σ(hθ(xⁱ) - yⁱ)²
梯度: ∂J/∂θⱼ = 1/m * Σ(hθ(xⁱ) - yⁱ) * xⱼⁱ
更新规则: θⱼ := θⱼ - α * ∂J/∂θⱼ
"""
print(f"使用梯度下降法求解 (学习率: {self.learning_rate})...")
m = X.shape[0] # 样本数量
n = X.shape[1] # 特征数量
# 添加截距项
if self.add_intercept:
X_with_intercept = self._add_intercept(X)
n += 1 # 特征数量加1(截距)
else:
X_with_intercept = X
# 初始化参数
self.theta = np.zeros(n)
# 梯度下降
for iteration in range(self.n_iterations):
# 预测值
y_pred = X_with_intercept @ self.theta
# 计算损失(均方误差)
loss = np.mean((y_pred - y) ** 2) / 2
self.loss_history.append(loss)
# 计算梯度
gradient = (1/m) * X_with_intercept.T @ (y_pred - y)
# 更新参数
self.theta -= self.learning_rate * gradient
# 检查收敛
if iteration > 0 and abs(self.loss_history[-2] - loss) < self.tolerance:
print(f"在第 {iteration} 次迭代收敛")
break
# 每100次迭代打印进度
if iteration % 100 == 0:
print(f"迭代 {iteration}: 损失 = {loss:.6f}")
# 计算统计量
self._calculate_statistics(X_with_intercept, y)
def _calculate_statistics(self, X, y):
"""
计算统计推断指标
"""
m = X.shape[0] # 样本数量
n = X.shape[1] # 参数数量(包括截距)
# 预测值
y_pred = X @ self.theta
# 1. 计算残差
residuals = y - y_pred
# 2. 计算残差平方和(RSS)和总平方和(TSS)
RSS = np.sum(residuals ** 2)
TSS = np.sum((y - np.mean(y)) ** 2)
# 3. 计算R²
self.r_squared = 1 - RSS / TSS
# 4. 计算参数的标准误差
# σ² = RSS / (m - n) 无偏估计
sigma_squared = RSS / (m - n)
try:
# 参数协方差矩阵
X_T_X_inv = np.linalg.inv(X.T @ X)
theta_cov = sigma_squared * X_T_X_inv
# 标准误差是协方差矩阵对角线的平方根
self.standard_errors = np.sqrt(np.diag(theta_cov))
# 5. 计算t统计量和p值
t_statistics = self.theta / self.standard_errors
# 计算p值(双侧检验)
from scipy import stats
self.p_values = 2 * (1 - stats.t.cdf(np.abs(t_statistics), df=m-n))
# 6. 计算95%置信区间
t_critical = stats.t.ppf(0.975, df=m-n) # 95%置信水平
self.confidence_intervals = np.column_stack([
self.theta - t_critical * self.standard_errors,
self.theta + t_critical * self.standard_errors
])
except np.linalg.LinAlgError:
print("警告: 无法计算统计推断(矩阵不可逆)")
def fit(self, X, y):
"""
拟合模型
"""
if self.method == 'ols':
self.fit_ols(X, y)
elif self.method == 'gradient':
self.fit_gradient_descent(X, y)
else:
raise ValueError(f"未知的方法: {self.method}")
def predict(self, X):
"""
预测
"""
if self.add_intercept:
X_with_intercept = self._add_intercept(X)
else:
X_with_intercept = X
return X_with_intercept @ self.theta
def summary(self, feature_names=None):
"""
输出模型摘要(类似statsmodels)
"""
if feature_names is None:
if self.add_intercept:
feature_names = ['Intercept'] + [f'x{i}' for i in range(len(self.theta)-1)]
else:
feature_names = [f'x{i}' for i in range(len(self.theta))]
print("=" * 60)
print("线性回归结果摘要")
print("=" * 60)
print(f"方法: {self.method.upper()}")
print(f"R²: {self.r_squared:.6f}")
print(f"调整R²: {1 - (1-self.r_squared)*(len(y)-1)/(len(y)-len(self.theta)):.6f}")
print(f"样本数量: {len(self.loss_history) if self.method=='gradient' else 'N/A'}")
print("\n系数:")
print("-" * 60)
print(f"{'特征':<15} {'系数':<12} {'标准误':<12} {'t值':<12} {'P>|t|':<12}")
print("-" * 60)
for i, (name, coef, se, p) in enumerate(zip(
feature_names,
self.theta,
self.standard_errors if self.standard_errors is not None else ['N/A']*len(self.theta),
self.p_values if self.p_values is not None else ['N/A']*len(self.theta)
)):
t_val = coef/se if se != 'N/A' else 'N/A'
p_str = f"{p:.6f}" if p != 'N/A' else 'N/A'
print(f"{name:<15} {coef:>12.6f} {se:>12.6f} {t_val:>12.6f} {p_str:>12}")
print("=" * 60)
# 使用自定义模型
print("=== 详细线性回归实现 ===")
detailed_model = DetailedLinearRegression(method='ols')
detailed_model.fit(X_train.values, y_train.values)
# 输出摘要
feature_names = preprocess_info['feature_names']
if detailed_model.add_intercept:
feature_names = ['Intercept'] + feature_names
detailed_model.summary(feature_names)
# 绘制梯度下降过程(如果使用梯度下降)
if detailed_model.method == 'gradient':
plt.figure(figsize=(10, 6))
plt.plot(detailed_model.loss_history)
plt.xlabel('迭代次数')
plt.ylabel('损失')
plt.title('梯度下降过程')
plt.grid(True, alpha=0.3)
plt.show()
六、模型评估
1. 计算预测值
# 在测试集上预测
y_pred = model.predict(X_test)
2. 使用评估指标
必须计算的指标:
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
# 1. 均方误差(MSE)
mse = mean_squared_error(y_test, y_pred)
# 2. 均方根误差(RMSE)- 与目标变量同单位
rmse = np.sqrt(mse)
# 3. 平均绝对误差(MAE)
mae = mean_absolute_error(y_test, y_pred)
# 4. R²分数(决定系数)- 解释方差的比例
r2 = r2_score(y_test, y_pred)
# 5. 平均绝对百分比误差(MAPE)
mape = np.mean(np.abs((y_test - y_pred) / y_test)) * 100
3. 可视化评估
必须做的图表:
-
实际值 vs 预测值散点图
-
理想情况:点沿对角线分布
-
添加y=x参考线
-
-
残差图
-
横轴:预测值
-
纵轴:残差(实际值-预测值)
-
理想情况:残差随机分布,无模式
-
-
残差直方图
-
检查残差是否近似正态分布
-
4. 解释结果
模型解释:
-
系数解释:每个单位特征变化引起的房价变化
-
例如:面积系数=5000 → 面积每增加1㎡,房价增加5000元
-
-
特征重要性:系数绝对值越大,特征越重要
-
模型局限性:R²表示模型解释的方差比例
完整代码:
def comprehensive_model_evaluation(model, X_train, X_test, y_train, y_test, model_name="模型"):
"""
全面的模型评估
"""
from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error
# 训练集和测试集预测
y_train_pred = model.predict(X_train.values) if hasattr(model, 'predict') else model.predict(X_train)
y_test_pred = model.predict(X_test.values) if hasattr(model, 'predict') else model.predict(X_test)
# 计算各种评估指标
metrics = {}
# 训练集指标
metrics['train_mse'] = mean_squared_error(y_train, y_train_pred)
metrics['train_rmse'] = np.sqrt(metrics['train_mse'])
metrics['train_mae'] = mean_absolute_error(y_train, y_train_pred)
metrics['train_mape'] = mean_absolute_percentage_error(y_train, y_train_pred)
metrics['train_r2'] = r2_score(y_train, y_train_pred)
# 测试集指标
metrics['test_mse'] = mean_squared_error(y_test, y_test_pred)
metrics['test_rmse'] = np.sqrt(metrics['test_mse'])
metrics['test_mae'] = mean_absolute_error(y_test, y_test_pred)
metrics['test_mape'] = mean_absolute_percentage_error(y_test, y_test_pred)
metrics['test_r2'] = r2_score(y_test, y_test_pred)
# 计算调整R²
n_train = X_train.shape[0]
p_train = X_train.shape[1]
n_test = X_test.shape[0]
p_test = X_test.shape[1]
metrics['train_adj_r2'] = 1 - (1-metrics['train_r2']) * (n_train-1) / (n_train-p_train-1)
metrics['test_adj_r2'] = 1 - (1-metrics['test_r2']) * (n_test-1) / (n_test-p_test-1)
# 创建评估报告
evaluation_report = pd.DataFrame({
'指标': ['MSE', 'RMSE', 'MAE', 'MAPE', 'R²', '调整R²'],
'训练集': [
f"{metrics['train_mse']:.2f}",
f"{metrics['train_rmse']:.2f}",
f"{metrics['train_mae']:.2f}",
f"{metrics['train_mape']:.4f}",
f"{metrics['train_r2']:.4f}",
f"{metrics['train_adj_r2']:.4f}"
],
'测试集': [
f"{metrics['test_mse']:.2f}",
f"{metrics['test_rmse']:.2f}",
f"{metrics['test_mae']:.2f}",
f"{metrics['test_mape']:.4f}",
f"{metrics['test_r2']:.4f}",
f"{metrics['test_adj_r2']:.4f}"
]
})
print(f"\n=== {model_name} 评估报告 ===")
print(evaluation_report.to_string(index=False))
# 可视化评估
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
# 1. 实际值 vs 预测值(测试集)
axes[0, 0].scatter(y_test, y_test_pred, alpha=0.6, s=30)
axes[0, 0].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()],
'r--', lw=2, label='完美预测线')
axes[0, 0].set_xlabel('实际房价')
axes[0, 0].set_ylabel('预测房价')
axes[0, 0].set_title('测试集: 实际值 vs 预测值')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
# 2. 残差分布
residuals_test = y_test - y_test_pred
axes[0, 1].hist(residuals_test, bins=30, edgecolor='black', alpha=0.7)
axes[0, 1].axvline(x=0, color='r', linestyle='--')
axes[0, 1].set_xlabel('残差')
axes[0, 1].set_ylabel('频率')
axes[0, 1].set_title('测试集残差分布')
axes[0, 1].grid(True, alpha=0.3)
# 3. 残差 vs 预测值
axes[0, 2].scatter(y_test_pred, residuals_test, alpha=0.6, s=30)
axes[0, 2].axhline(y=0, color='r', linestyle='--')
axes[0, 2].set_xlabel('预测值')
axes[0, 2].set_ylabel('残差')
axes[0, 2].set_title('残差 vs 预测值')
axes[0, 2].grid(True, alpha=0.3)
# 4. 学习曲线(训练集和测试集误差随样本数的变化)
from sklearn.model_selection import learning_curve
train_sizes, train_scores, test_scores = learning_curve(
model, X_train, y_train, cv=5,
scoring='neg_mean_squared_error',
train_sizes=np.linspace(0.1, 1.0, 10)
)
train_scores_mean = -train_scores.mean(axis=1)
test_scores_mean = -test_scores.mean(axis=1)
axes[1, 0].plot(train_sizes, train_scores_mean, 'o-', label='训练集')
axes[1, 0].plot(train_sizes, test_scores_mean, 'o-', label='测试集')
axes[1, 0].set_xlabel('训练样本数')
axes[1, 0].set_ylabel('MSE')
axes[1, 0].set_title('学习曲线')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)
# 5. 特征重要性(系数绝对值)
if hasattr(model, 'coef_'):
coef_abs = np.abs(model.coef_)
feature_names = X_train.columns
# 只显示前10个最重要的特征
idx = np.argsort(coef_abs)[-10:]
axes[1, 1].barh(range(len(idx)), coef_abs[idx])
axes[1, 1].set_yticks(range(len(idx)))
axes[1, 1].set_yticklabels([feature_names[i] for i in idx])
axes[1, 1].set_xlabel('系数绝对值')
axes[1, 1].set_title('特征重要性(前10)')
# 6. 预测误差分布
error_percentage = (residuals_test / y_test) * 100
axes[1, 2].hist(error_percentage, bins=30, edgecolor='black', alpha=0.7)
axes[1, 2].axvline(x=0, color='r', linestyle='--')
axes[1, 2].set_xlabel('预测误差 (%)')
axes[1, 2].set_ylabel('频率')
axes[1, 2].set_title('预测误差百分比分布')
axes[1, 2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 返回评估指标
return metrics
# 评估详细模型
metrics_detailed = comprehensive_model_evaluation(
detailed_model, X_train, X_test, y_train, y_test, "详细线性回归"
)
# 使用sklearn模型进行对比
from sklearn.linear_model import LinearRegression
sklearn_model = LinearRegression()
sklearn_model.fit(X_train, y_train)
metrics_sklearn = comprehensive_model_evaluation(
sklearn_model, X_train, X_test, y_train, y_test, "Scikit-learn线性回归"
)
七、模型优化
1. 正则化(防止过拟合)
三种方法:
from sklearn.linear_model import Ridge, Lasso, ElasticNet
# 1. 岭回归(Ridge)- L2正则化
ridge = Ridge(alpha=1.0) # alpha是正则化强度
# 2. LASSO回归 - L1正则化(可做特征选择)
lasso = Lasso(alpha=0.1)
# 3. 弹性网络 - L1和L2组合
elastic = ElasticNet(alpha=0.1, l1_ratio=0.5)
2. 特征选择
方法:
-
基于统计检验:选择与目标变量显著相关的特征
-
递归特征消除:逐步移除最不重要的特征
-
LASSO回归:自动将不重要特征的系数压缩为0
3. 多项式回归(处理非线性)
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
# 创建多项式回归管道
poly_model = Pipeline([
('poly', PolynomialFeatures(degree=2)), # 二次多项式
('linear', LinearRegression())
])
4. 交叉验证
评估模型稳定性:
from sklearn.model_selection import cross_val_score
# 5折交叉验证
scores = cross_val_score(model, X_train, y_train,
cv=5, scoring='r2')
print(f"交叉验证R²: {scores.mean():.4f} (±{scores.std():.4f})")
八、模型部署
1. 保存加载模型
import joblib
import json
import pickle
def save_model_pipeline(model, preprocess_info, model_name='housing_price_model'):
"""
保存完整的模型管道
"""
import os
from datetime import datetime
# 创建保存目录
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
save_dir = f'models/{model_name}_{timestamp}'
os.makedirs(save_dir, exist_ok=True)
# 1. 保存模型
model_path = f'{save_dir}/model.pkl'
if hasattr(model, 'save'):
model.save(model_path)
else:
with open(model_path, 'wb') as f:
pickle.dump(model, f)
# 2. 保存预处理信息
preprocess_path = f'{save_dir}/preprocess_info.json'
with open(preprocess_path, 'w') as f:
# 转换numpy数组为列表
preprocess_info_serializable = {}
for key, value in preprocess_info.items():
if isinstance(value, np.ndarray):
preprocess_info_serializable[key] = value.tolist()
elif hasattr(value, '__dict__'):
preprocess_info_serializable[key] = str(value)
else:
preprocess_info_serializable[key] = value
json.dump(preprocess_info_serializable, f, indent=2)
# 3. 保存模型性能
y_pred = model.predict(X_test.values) if hasattr(model, 'predict') else model.predict(X_test)
performance = {
'test_r2': float(r2_score(y_test, y_pred)),
'test_rmse': float(np.sqrt(mean_squared_error(y_test, y_pred))),
'test_mae': float(mean_absolute_error(y_test, y_pred)),
'train_size': X_train.shape[0],
'test_size': X_test.shape[0],
'n_features': X_train.shape[1],
'model_type': type(model).__name__,
'save_time': timestamp
}
performance_path = f'{save_dir}/performance.json'
with open(performance_path, 'w') as f:
json.dump(performance, f, indent=2)
# 4. 保存特征重要性(如果可用)
if hasattr(model, 'coef_'):
feature_importance = pd.DataFrame({
'feature': preprocess_info['feature_names'],
'coefficient': model.coef_,
'abs_coefficient': np.abs(model.coef_)
}).sort_values('abs_coefficient', ascending=False)
feature_importance.to_csv(f'{save_dir}/feature_importance.csv', index=False)
print(f"模型已保存到: {save_dir}")
print(f"性能: R²={performance['test_r2']:.4f}, RMSE={performance['test_rmse']:.2f}")
return save_dir
def load_model_pipeline(model_dir):
"""
加载模型管道
"""
# 1. 加载模型
model_path = f'{model_dir}/model.pkl'
with open(model_path, 'rb') as f:
model = pickle.load(f)
# 2. 加载预处理信息
preprocess_path = f'{model_dir}/preprocess_info.json'
with open(preprocess_path, 'r') as f:
preprocess_info = json.load(f)
# 3. 加载性能
performance_path = f'{model_dir}/performance.json'
with open(performance_path, 'r') as f:
performance = json.load(f)
print(f"模型加载成功: {model_dir}")
print(f"模型类型: {performance['model_type']}")
print(f"性能: R²={performance['test_r2']:.4f}, RMSE={performance['test_rmse']:.2f}")
return model, preprocess_info, performance
# 保存模型
model_save_dir = save_model_pipeline(detailed_model, preprocess_info)
# 加载模型(示例)
# loaded_model, loaded_preprocess_info, loaded_performance = load_model_pipeline(model_save_dir)
创建预测API
class HousingPricePredictor:
"""
房价预测器类(可用于生产环境)
"""
def __init__(self, model, preprocess_info):
self.model = model
self.preprocess_info = preprocess_info
self.scaler = preprocess_info.get('scaler')
self.numeric_features = preprocess_info.get('numeric_features', [])
self.categorical_features = preprocess_info.get('categorical_features', [])
self.feature_names = preprocess_info.get('feature_names', [])
print(f"预测器初始化完成")
print(f"特征数量: {len(self.feature_names)}")
print(f"数值特征: {self.numeric_features}")
print(f"分类特征: {self.categorical_features}")
def preprocess_input(self, input_data):
"""
预处理输入数据
"""
# 将输入转换为DataFrame
if isinstance(input_data, dict):
df = pd.DataFrame([input_data])
elif isinstance(input_data, pd.DataFrame):
df = input_data.copy()
else:
raise ValueError("输入数据必须是字典或DataFrame")
# 检查必要的特征
missing_features = set(self.numeric_features + self.categorical_features) - set(df.columns)
if missing_features:
raise ValueError(f"缺少特征: {missing_features}")
# 分离数值和分类特征
numeric_data = df[self.numeric_features].copy()
categorical_data = df[self.categorical_features].copy() if self.categorical_features else None
# 处理分类特征(编码)
if categorical_data is not None and len(categorical_data) > 0:
# 创建所有可能的哑变量列
encoded_dfs = []
for col in self.categorical_features:
if col in categorical_data.columns:
# 为每个分类特征创建哑变量
dummies = pd.get_dummies(categorical_data[col], prefix=col)
# 确保有所有训练时出现的类别
expected_columns = [c for c in self.feature_names if c.startswith(f"{col}_")]
for expected_col in expected_columns:
if expected_col not in dummies.columns:
dummies[expected_col] = 0
# 只保留训练时出现的列
dummies = dummies[expected_columns]
encoded_dfs.append(dummies)
if encoded_dfs:
encoded_data = pd.concat(encoded_dfs, axis=1)
# 合并数值特征和编码后的分类特征
processed_data = pd.concat([numeric_data, encoded_data], axis=1)
else:
processed_data = numeric_data
else:
processed_data = numeric_data
# 确保列的顺序与训练时一致
processed_data = processed_data[self.feature_names]
# 特征缩放
if self.scaler is not None:
processed_data_scaled = self.scaler.transform(processed_data)
else:
processed_data_scaled = processed_data.values
return processed_data_scaled
def predict(self, input_data, return_confidence=False):
"""
预测房价
"""
# 预处理
X_processed = self.preprocess_input(input_data)
# 预测
predictions = self.model.predict(X_processed)
if return_confidence and hasattr(self.model, 'standard_errors'):
# 计算置信区间(如果模型支持)
if self.model.standard_errors is not None:
# 简单实现:使用标准误差的倍数
std_error = np.mean(self.model.standard_errors[1:]) # 排除截距
confidence_interval = 1.96 * std_error # 95%置信区间
result = {
'prediction': float(predictions[0]),
'confidence_interval': float(confidence_interval),
'lower_bound': float(predictions[0] - confidence_interval),
'upper_bound': float(predictions[0] + confidence_interval)
}
return result
return float(predictions[0]) if len(predictions) == 1 else predictions
def explain_prediction(self, input_data):
"""
解释预测结果(特征贡献分析)
"""
if not hasattr(self.model, 'theta'):
return "模型不支持解释功能"
# 预处理
X_processed = self.preprocess_input(input_data)
# 获取模型参数
if hasattr(self.model, 'theta'):
coefficients = self.model.theta[1:] # 排除截距
elif hasattr(self.model, 'coef_'):
coefficients = self.model.coef_
else:
return "无法获取模型系数"
intercept = self.model.intercept_ if hasattr(self.model, 'intercept_') else self.model.theta[0]
# 计算每个特征的贡献
feature_contributions = X_processed[0] * coefficients
# 创建贡献分析
contribution_df = pd.DataFrame({
'特征': self.feature_names,
'值': X_processed[0],
'系数': coefficients,
'贡献': feature_contributions,
'贡献百分比': np.abs(feature_contributions) / np.sum(np.abs(feature_contributions)) * 100
}).sort_values('贡献百分比', ascending=False)
print("=== 预测结果解释 ===")
print(f"预测房价: {self.predict(input_data):.2f} 元")
print(f"基准值 (截距): {intercept:.2f} 元")
print("\n特征贡献分析:")
print(contribution_df.to_string(index=False))
# 可视化贡献
top_n = min(10, len(contribution_df))
top_contributions = contribution_df.head(top_n)
plt.figure(figsize=(10, 6))
bars = plt.barh(range(top_n), top_contributions['贡献百分比'].values[::-1])
plt.yticks(range(top_n), top_contributions['特征'].values[::-1])
plt.xlabel('贡献百分比 (%)')
plt.title('特征贡献分析 (前10个最重要的特征)')
# 在条形上添加数值
for i, (bar, contrib) in enumerate(zip(bars, top_contributions['贡献百分比'].values[::-1])):
plt.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height()/2,
f'{contrib:.1f}%', va='center')
plt.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.show()
return contribution_df
# 创建预测器实例
predictor = HousingPricePredictor(detailed_model, preprocess_info)
# 测试预测器
test_house = {
'面积': 120,
'卧室数': 3,
'卫生间数': 2,
'楼层数': 2,
'房龄': 10,
'距离市中心': 8,
'学区评分': 7.5
}
# 添加分类特征(如果原始数据有)
if '房龄分段' in data.columns:
test_house['房龄分段'] = '次新房'
if '面积分段' in data.columns:
test_house['面积分段'] = '中户型'
print("=== 单样本预测测试 ===")
prediction = predictor.predict(test_house)
print(f"预测房价: {prediction:.2f} 元")
# 解释预测
contribution_df = predictor.explain_prediction(test_house)
# 批量预测
print("\n=== 批量预测测试 ===")
test_houses = pd.DataFrame([
{'面积': 80, '卧室数': 2, '卫生间数': 1, '楼层数': 1, '房龄': 5, '距离市中心': 3, '学区评分': 8},
{'面积': 150, '卧室数': 4, '卫生间数': 3, '楼层数': 2, '房龄': 15, '距离市中心': 15, '学区评分': 6},
{'面积': 200, '卧室数': 5, '卫生间数': 4, '楼层数': 3, '房龄': 3, '距离市中心': 25, '学区评分': 9}
])
# 添加分类特征
if '房龄分段' in data.columns:
test_houses['房龄分段'] = pd.cut(test_houses['房龄'],
bins=[0, 5, 10, 20, 50],
labels=['新房', '次新房', '旧房', '老房'])
if '面积分段' in data.columns:
test_houses['面积分段'] = pd.cut(test_houses['面积'],
bins=[0, 80, 120, 180, 500],
labels=['小户型', '中户型', '大户型', '豪宅'])
batch_predictions = []
for i, house in test_houses.iterrows():
pred = predictor.predict(house.to_dict())
batch_predictions.append(pred)
print(f"房屋 {i+1}: {pred:.2f} 元")
test_houses['预测房价'] = batch_predictions
print("\n批量预测结果:")
print(test_houses[['面积', '卧室数', '房龄', '预测房价']])
九、结论与总结
1 技术总结
-
数据准备是关键
-
数据质量直接影响模型性能
-
特征工程可以显著提升模型表现
-
数据清洗和预处理是必须的步骤
-
-
模型选择与调优
-
线性回归是良好的基准模型
-
正则化方法(岭回归、LASSO)可以防止过拟合
-
多项式回归可以捕捉非线性关系
-
-
模型评估的重要性
-
使用多个评估指标(R²、RMSE、MAE、MAPE)
-
交叉验证提供更稳健的性能估计
-
残差分析帮助验证模型假设
-
-
模型解释与部署
-
线性回归具有优秀的可解释性
-
特征重要性分析帮助理解模型
-
模型部署需要考虑生产环境的需求
-
2 业务价值
-
自动化估值:快速、一致地评估房屋价格
-
风险控制:银行可以更准确地评估抵押贷款风险
-
市场分析:分析影响房价的关键因素
-
投资决策:帮助投资者识别被低估的房产
3 局限性与改进方向
-
局限性
-
线性假设可能过于简单
-
对异常值敏感
-
无法自动处理非线性关系
-
-
改进方向
-
使用更复杂的模型(随机森林、梯度提升、神经网络)
-
集成学习方法
-
添加更多特征(地理位置、周边设施等)
-
考虑时间因素(时间序列分析)
-
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐




所有评论(0)