题目: Programming Exercise 1: Linear Regression 课程 : Machine Learning Specialization by Andrew Ng
作业背景 假设你是一家餐饮连锁店的CEO,正在考虑在不同城市开设新的食品车网点。你希望将业务扩展到能够带来更高利润的城市。
连锁店已经在各个城市有餐厅,你拥有这些城市的利润和人口数据
你还有一些候选城市的人口数据
目标 :使用数据帮助你识别哪些城市可能为你的业务带来更高的利润
数据集说明 文件 : ex1data1.txt
x_train : 城市人口(单位:万人)
例如:6.1101 表示该城市人口为 61,101 人
y_train : 该城市餐厅的月平均利润(单位:万美元)
例如:17.592 表示月平均利润为 $175,920
负值表示亏损,如 -2.6807 表示月平均亏损 $26,807
数据规模 : 97个训练样本
Exercise 1: 实现代价函数(Compute Cost) 任务 : 完成compute_cost函数,计算线性回归的代价函数J(w,b)
代价函数公式 :
其中:
Exercise 2: 实现梯度计算(Compute Gradient) 任务 : 完成compute_gradient函数,计算代价函数对参数的梯度
梯度公式 :
梯度下降参数设置
学习率 α = 0.01
迭代次数 = 1500
初始值:w = 0, b = 0
预期结果 :
最终参数:w ≈ 1.166, b ≈ -3.630
人口35,000的城市预测利润:$4,519.77
人口70,000的城市预测利润:$45,342.45
单变量 Simple variable 代价函数cost function 参数表示法说明 在机器学习中,有两种常见的参数表示方法:
吴恩达课程表示法 : (w是权重weight,b是偏置bias)
统一向量表示法 : (使用theta向量)
参数对应关系 :
为了使用统一的theta表示法,我们需要在原始数据前添加一列全1(偏置列),这样可以将参数写成向量形式:
代价函数公式(theta表示法) 假设函数(Hypothesis Function) :
对于m个训练样本,代价函数(Cost Function) 为:
展开后:
向量化表示 :
其中:
是 m×2 的矩阵(第一列全为1,第二列为原始特征x)
是 2×1 的参数向量
是 m×1 的目标值向量
梯度下降gradient descent 梯度计算(theta表示法) 对代价函数求偏导数:
对 的偏导数 :
对 的偏导数 :
统一的向量形式 :
其中 (对应偏置项)
梯度下降更新规则 批量梯度下降(Batch Gradient Descent) :
重复执行直到收敛:
同时更新所有参数(j = 0, 1):
向量化形式 :
其中α是学习率(learning rate)
过程: 1. 导入必要的库 机器学习项目需要三个核心库:
pandas :数据处理和分析,提供DataFrame结构
matplotlib :数据可视化,绘制各种图表
numpy :科学计算,矩阵运算必备
1 2 3 4 import pandas as pdimport numpy as npimport matplotlib.pyplot as plt%matplotlib inline
这里的as 就是缩写的意思 将numpy缩写成np,方便写代码!(好吧本人基础真的很烂,,,我其实现在才知道😂)
2. 读取数据 - pd.read_csv() pd.read_csv()是pandas最常用的函数之一,用于读取CSV和其他文本数据。
重要参数 :
names=['列名1', '列名2']:给没有表头的数据添加列名(注意是names不是name!我就写成name…马上就报错)
header=None:告诉pandas文件没有表头,有表头方便处理数据
sep=',':指定分隔符(默认逗号)
1 2 3 4 5 6 7 8 9 10 11 12 data = pd.read_csv('ex1data1.txt' , names=['Population' , 'Profit' ])
3. 查看数据 - .head() 和 .tail() 获取数据后的第一件事:查看数据长什么样! (这个很重要,b站的up主告诉我的)
.head(n):查看前n行数据(默认5行)
.tail(n):查看后n行数据(默认5行)
4. 了解数据结构 - .info() .info()提供数据的完整结构信息,这一步非常重要 !
pandas是DataFrame结构,numpy是ndarray结构 ,两者经常需要转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 data.info() print (data.dtypes)X = data['Population' ].values print (type (X)) print (X.shape) data_array = data.values print (type (data_array)) print (data_array.shape)
注意事项 :
DataFrame适合数据预处理和探索
numpy数组适合数值计算
很多机器学习算法需要numpy数组格式
结构不匹配是常见错误来源!
5. 数据可视化 - 绘图函数plot和ax 可视化是理解数据的关键!matplotlib提供了两种绘图方式: 我其实不太会用,每次就背下来或者复制粘贴~
方式1:使用pyplot接口(简单快速)
1 2 3 4 5 6 plt.scatter(data['Population' ], data['Profit' ], marker='x' , c='r' ) plt.xlabel('Population of City in 10,000s' ) plt.ylabel('Profit in $10,000s' ) plt.title('Scatter plot of training data' ) plt.show()
方式2:使用面向对象的方式(更灵活)
1 2 3 4 5 6 7 8 9 10 fig, ax = plt.subplots(figsize=(8 , 6 )) ax.scatter(data['Population' ], data['Profit' ], marker='x' , c='r' , label='Training data' ) ax.set_xlabel('Population of City in 10,000s' ) ax.set_ylabel('Profit in $10,000s' ) ax.set_title('Scatter plot of training data' ) ax.legend() plt.show()
常用参数说明 :
marker='x':标记样式(’o’, ‘x’, ‘*’, ‘^’等)
c='r':颜色(’r’红色, ‘b’蓝色, ‘g’绿色)
figsize=(8,6):图形大小(宽,高)
label='名称':图例标签
6. 参数表示法转换 - 从(w,b)到θ向量 重要概念(其实也是看b站看到的,不然我可能还在用W,B来写代码) : 吴恩达课程中:
w是weight(权重/斜率)
b是bias(偏置/截距)
统一向量表示法:
θ₀ = b (偏置项/截距)
θ₁ = w (权重/斜率)
为什么要统一? 将参数写成向量形式后,公式推导和代码实现都更简洁:
7. 添加偏置列 - 数据预处理的关键步骤 为了使用向量化计算,需要在特征矩阵X前面添加一列全1(偏置列)。这样:
原始数据:x(只有人口数据)
处理后:X = [1, x](第一列全是1,第二列是人口数据)
为什么要添加偏置列?
矩阵形式的详细说明 为了能用一次矩阵乘法完成所有计算,我们需要巧妙地构造矩阵:
1. 参数向量 :
其中θ₀ = b(偏置/截距),θ₁ = w(权重/斜率)
2. 原始特征向量 (m个样本):
3. 改造后的特征矩阵 (添加偏置列):
4. 矩阵乘法计算预测值 :
这样,矩阵乘法的结果X@theta正好就是我们想要的wx + b!
三种添加偏置列的方法(针对NumPy数组):
方法1:np.column_stack (最推荐✅) 这个函数专门用于将一维或二维数组按列合并。它非常适合我们的需求。
工作原理 : 它接收一个由数组组成的列表或元组 [array1, array2, ...],然后将它们像柱子一样并排堆叠起来。它能很智能地处理一维数组,自动将其视为列向量。
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 ones = np.ones(m) X = np.column_stack([ones, x]) print (X)
优点 :
代码意图清晰 :函数名 column_stack(列堆叠)直接说明了操作的目的
简洁方便 :一行代码即可完成,并且不需要预先改变输入数组 x 的形状
方法2:np.insert 这个函数更加通用,它可以在数组的任意位置(行或列)插入指定的值。
工作原理 : np.insert(array, index, values, axis)
array: 要操作的原始数组
index: 要插入的位置索引
values: 要插入的值
axis: 沿哪个轴插入。axis=0 表示插入行,axis=1 表示插入列
重要 :当使用 axis=1 插入列时,原始数组必须是二维的 。因此,需要先将一维的 x 转换成二维的列向量(形状从 (m,) 变为 (m, 1))。
代码实现 :
1 2 3 4 5 6 x_reshaped = x.reshape(-1 , 1 ) X = np.insert(x_reshaped, 0 , 1 , axis=1 )
优点 : 功能强大,可以在任意位置插入缺点 : 需要先reshape,代码稍显繁琐
方法3:手动创建和赋值(最基础) 这种方法最能体现底层逻辑,对理解NumPy索引非常有帮助。
1 2 3 4 5 6 7 8 X = np.zeros((m, 2 )) X[:, 0 ] = 1 X[:, 1 ] = x
针对DataFrame结构的处理方法:
如果数据是DataFrame结构,有两种主要方法添加偏置列:
方法一:直接赋值 (最简单、最常用) 这是向 DataFrame 添加新列的最标准方法。Pandas 会自动将你赋的单个值(比如 1)“广播”到所有行。
1 2 3 4 5 df['bias' ] = 1 print ("\n直接赋值后的DataFrame:" )print (df)
输出 (新列默认添加在末尾):
1 2 3 4 5 直接赋值后的DataFrame: population profit bias 0 6.1101 17.5920 1 1 5.5277 9.1302 1 2 8.5186 13.6620 1
方法二:使用 df.insert() (可以指定位置) 如果你希望将偏置列放在第一列(这在机器学习中很常见),insert() 方法是完美的选择。
df.insert(loc, column_name, value)
1 2 3 4 5 df.insert(0 , 'bias' , 1 ) print ("\n使用 insert 后的DataFrame:" )print (df)
输出:
1 2 3 4 5 使用 insert 后的DataFrame: bias population profit 0 1 6.1101 17.5920 1 1 5.5277 9.1302 2 1 8.5186 13.6620
7.在监督学习(比如线性回归)中,我们的数据集通常包含两部分:
**特征 (Features, 通常用 X 表示)**:用来进行预测的输入变量。例如,在这个作业里就是”城市人口”。在更复杂的问题中,可能会有多个特征,比如”城市人口”、”人均收入”、”餐厅数量”等。
**目标 (Target, 通常用 y 表示)**:我们想要预测的变量。例如,作业中的”餐厅利润”。
通常,当我们从一个文件(如 CSV)中加载数据时,所有的特征和目标都放在同一个表格里。我们的模型训练函数需要接收分开的 X 和 y。
因此,切片操作的核心目的就是:从原始的完整数据表中,把特征(X)和目标(y)分离出来。
切片操作详解 - DataFrame和NumPy结构 针对DataFrame结构的切片 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 data = pd.DataFrame({ 'Population' : [6.1101 , 5.5277 , 8.5186 , 7.0032 , 5.8598 ], 'Profit' : [17.5920 , 9.1302 , 13.6620 , 11.8540 , 6.8233 ] }) X = data.iloc[:, :-1 ] y = data.iloc[:, -1 ] X = data[['Population' ]] y = data['Profit' ] X = data.drop('Profit' , axis=1 ) y = data['Profit' ] X_array = X.values y_array = y.values print (f"X shape: {X_array.shape} " ) print (f"y shape: {y_array.shape} " )
针对NumPy数组的切片 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 data = np.loadtxt('ex1data1.txt' , delimiter=',' ) data = pd.read_csv('ex1data1.txt' , names=['Population' , 'Profit' ]).values X = data[:, 0 ] y = data[:, 1 ] print (f"X shape: {X.shape} " ) print (f"y shape: {y.shape} " ) X = X.reshape(-1 , 1 )
切片后的数据处理流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def prepare_data (filename ): """完整的数据准备流程""" data = pd.read_csv(filename, names=['Population' , 'Profit' ]) X = data['Population' ].values y = data['Profit' ].values m = len (y) X = np.column_stack([np.ones(m), X]) theta = np.zeros(2 ) return X, y, theta, m X, y, theta, m = prepare_data('ex1data1.txt' ) print (f"数据准备完成:" )print (f" 样本数量: {m} " )print (f" 特征矩阵X形状: {X.shape} " )print (f" 目标向量y形状: {y.shape} " )print (f" 参数theta形状: {theta.shape} " )
重要提醒 :
DataFrame切片后记得用.values或.to_numpy()转换为NumPy数组
注意维度:一维数组(n,)vs二维数组(n,1)
切片是数据预处理的必要步骤,不可省略!
8. NumPy数组创建函数对比 - zeros、ones、arange、linspace 在机器学习中,经常需要创建各种初始数组。理解不同创建函数的区别很重要!
np.zeros() vs np.zeros(()) 关键区别 :参数传递方式不同!
np.zeros(shape) - shape是一个整数或元组
当shape是单个整数时,创建一维数组
当shape是元组时,创建多维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import numpy as npa = np.zeros(5 ) print (a) print (a.shape) b = np.zeros((5 , 2 )) print (b)print (b.shape) np.zeros((5 , 2 )) c = np.zeros((2 , 3 , 4 )) print (c.shape)
常用数组创建函数对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 zeros_1d = np.zeros(5 ) zeros_2d = np.zeros((3 , 2 )) ones_1d = np.ones(5 ) ones_2d = np.ones((3 , 2 )) arr1 = np.arange(5 ) arr2 = np.arange(2 , 10 ) arr3 = np.arange(0 , 10 , 2 ) lin1 = np.linspace(0 , 10 , 5 ) lin2 = np.linspace(0 , 1 , 11 ) rand_uniform = np.random.rand(3 , 2 ) rand_normal = np.random.randn(3 , 2 ) rand_int = np.random.randint(0 , 10 , size=(3 , 2 ))
实际应用场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 m = 97 n = 2 theta = np.zeros(n) bias = np.ones(m) population_test = np.linspace(4 , 24 , 100 ) batch_size = 32 indices = np.arange(0 , m, batch_size) input_size = 10 hidden_size = 5 W = np.random.randn(input_size, hidden_size) * 0.01 b = np.zeros(hidden_size)
形状(shape)陷阱总结 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 a = np.zeros(5 ) b = np.zeros((5 ,)) c = np.zeros((5 , 1 )) d = np.zeros((1 , 5 )) x = np.arange(6 ) x_2d = x.reshape(2 , 3 ) x_col = x.reshape(-1 , 1 ) a = np.ones((3 , 1 )) b = np.ones(4 ) x = np.ones(5 ) theta = np.ones(5 ) result = np.dot(x, theta) X = np.ones((10 , 5 )) result = np.dot(X, theta)
记忆技巧 :
zeros/ones的参数:单个数字→一维,元组→多维
arange:不包含终点(像Python的range)
linspace:包含终点(线性等分)
使用reshape(-1, 1)将一维数组变成列向量
9. np.power()和np.sum() - 计算代价函数的利器 这两个函数在计算代价函数时经常配合使用!
np.power() - 计算幂运算 基本语法 :
1 np.power(base, exponent)
使用示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import numpy as npresult = np.power(3 , 4 ) errors = np.array([1 , -2 , 3 , -4 ]) squared = np.power(errors, 2 ) a = np.array([1 , 2 , 3 ]) method1 = np.power(a, 2 ) method2 = a ** 2 method3 = a * a bases = np.array([[1 , 2 ], [3 , 4 ]]) result = np.power(bases, 2 )
np.sum() - 求和函数(超级重要!因为公式需要求和。。。) 基本语法 :
1 np.sum (array, axis=None , keepdims=False )
参数说明 :
array:要求和的数组
axis:沿着哪个轴求和(None表示所有元素求和)
keepdims:是否保持维度
使用示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 arr = np.array([1 , 2 , 3 , 4 ]) total = np.sum (arr) matrix = np.array([[1 , 2 , 3 ], [4 , 5 , 6 ]]) sum_all = np.sum (matrix) sum_rows = np.sum (matrix, axis=1 ) sum_cols = np.sum (matrix, axis=0 ) a = np.array([[1 , 2 ], [3 , 4 ]]) sum_keepdims = np.sum (a, axis=1 , keepdims=True ) print (sum_keepdims.shape) sum_no_keepdims = np.sum (a, axis=1 , keepdims=False ) print (sum_no_keepdims.shape)
在代价函数中的应用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 def compute_cost_manual (X, y, theta ): """手动计算代价函数,展示power和sum的用法""" m = len (y) predictions = X.dot(theta) errors = predictions - y squared_errors = np.power(errors, 2 ) sum_squared_errors = np.sum (squared_errors) cost = sum_squared_errors / (2 * m) return cost def compute_cost_vectorized (X, y, theta ): """向量化计算代价函数""" m = len (y) errors = X.dot(theta) - y cost = np.sum (np.power(errors, 2 )) / (2 * m) return cost
梯度计算中的应用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def compute_gradient (X, y, theta ): """计算梯度,展示sum的axis用法""" m = len (y) errors = X.dot(theta) - y gradient = np.zeros(len (theta)) for j in range (len (theta)): gradient[j] = np.sum (errors * X[:, j]) / m gradient = X.T.dot(errors) / m return gradient
实际例子:完整的代价函数计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 m = 97 X = np.column_stack([np.ones(m), data['Population' ].values]) y = data['Profit' ].values theta = np.array([0 , 0 ]) predictions = X.dot(theta) errors = predictions - y squared_errors = np.power(errors, 2 ) total_error = np.sum (squared_errors) cost = total_error / (2 * m) print (f"初始代价: {cost:.2 f} " )alpha = 0.01 gradient = X.T.dot(errors) / m theta = theta - alpha * gradient new_cost = np.sum (np.power(X.dot(theta) - y, 2 )) / (2 * m) print (f"更新后代价: {new_cost:.2 f} " )
重要提醒 :
np.power(x, 2)和x**2功能相同,但**更常用
np.sum()的axis参数很重要,记住:axis=0向下,axis=1向右
在代价函数中,power用于计算平方误差,sum用于求总和
向量化计算比循环快得多!
10. DataFrame和NumPy结构互换 - 数据格式转换大全 在机器学习项目中,经常需要在pandas的DataFrame和NumPy的ndarray之间转换。掌握这些转换技巧非常重要!
为什么需要转换?
DataFrame :适合数据清理、探索性分析、特征工程
NumPy数组 :适合数值计算、机器学习算法输入
DataFrame → NumPy数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import pandas as pdimport numpy as npdf = pd.DataFrame({ 'Population' : [6.1101 , 5.5277 , 8.5186 ], 'Profit' : [17.5920 , 9.1302 , 13.6620 ] }) array1 = df.values print (type (array1)) print (array1.shape) array2 = df.to_numpy() print (type (array2)) print (array2.shape) X = df['Population' ].values y = df['Profit' ].values features = df[['Population' ]].values
NumPy数组 → DataFrame 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 data_array = np.array([[6.1101 , 17.5920 ], [5.5277 , 9.1302 ], [8.5186 , 13.6620 ]]) df1 = pd.DataFrame(data_array, columns=['Population' , 'Profit' ]) print (df1)population = np.array([6.1101 , 5.5277 , 8.5186 ]) profit = np.array([17.5920 , 9.1302 , 13.6620 ]) df2 = pd.DataFrame({ 'Population' : population, 'Profit' : profit }) df3 = pd.DataFrame(data_array, columns=['Population' , 'Profit' ], index=['City1' , 'City2' , 'City3' ])
Series和一维数组的转换 1 2 3 4 5 6 7 8 9 series = df['Population' ] arr = series.values arr = series.to_numpy() arr = np.array([1 , 2 , 3 , 4 , 5 ]) series = pd.Series(arr, name='my_data' )
维度问题处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 df = pd.DataFrame({'A' : [1 , 2 , 3 ]}) arr_1d = df['A' ].values arr_2d = df[['A' ]].values arr_1d = np.array([1 , 2 , 3 , 4 , 5 ]) arr_2d_col = arr_1d.reshape(-1 , 1 ) arr_2d_row = arr_1d.reshape(1 , -1 ) arr_1d = np.array([1 , 2 , 3 ]) arr_2d = arr_1d[:, np.newaxis]
实际应用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 def prepare_data_complete (filename ): """完整的数据准备流程,展示所有转换""" df = pd.read_csv(filename, names=['Population' , 'Profit' ]) print ("数据统计信息:" ) print (df.describe()) print ("\n数据类型:" ) print (df.dtypes) X = df['Population' ].values y = df['Profit' ].values data = df.values X_alt = data[:, 0 ] y_alt = data[:, 1 ] m = len (y) X = np.column_stack([np.ones(m), X]) theta = np.array([-3.630 , 1.166 ]) predictions = X.dot(theta) results = pd.DataFrame({ 'Population' : df['Population' ], 'Actual_Profit' : df['Profit' ], 'Predicted_Profit' : predictions, 'Error' : df['Profit' ] - predictions }) return X, y, results
类型转换注意事项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 df = pd.DataFrame({'A' : [1 , 2 , 3 ], 'B' : [4.5 , 5.5 , 6.5 ]}) arr = df.values print (arr.dtype) df_with_nan = pd.DataFrame({'A' : [1 , 2 , np.nan]}) arr_with_nan = df_with_nan.values arr_filled = df_with_nan.fillna(0 ).values df = pd.DataFrame({'A' : range (1000000 )}) arr = df.values columns = df.columns.tolist() index = df.index.tolist()
最佳实践总结 :
数据加载和预处理用DataFrame
数值计算和模型训练用NumPy数组
结果展示和分析再转回DataFrame
注意维度:df['col']→一维,df[['col']]→二维
使用.to_numpy()代替.values(更明确)
处理缺失值后再转换为NumPy数组
多变量 Multi variable 多变量线性回归就是有多个特征(features)的回归问题!比如预测房价时,不只考虑房屋面积,还要考虑卧室数量、楼层、建造年份等。(本次作业好像是size、bedroom)
多变量假设函数 符号约定
n : 特征数量(不包括偏置项x₀=1)
m : 训练样本数量
x^(i) : 第i个训练样本的特征向量
x_j^(i) : 第i个样本的第j个特征值
假设函数表达式 多变量假设函数 :
向量化表示 (推荐!):
其中:
是(n+1)×1的参数向量
是(n+1)×1的特征向量,其中
代价函数(多变量) 代价函数形式与单变量相同,只是现在处理的是向量:
向量化形式 :
其中:
X是m×(n+1)的设计矩阵(design matrix)
每一行是一个训练样本(含偏置项)
特征归一化 Feature Normalization(超级重要!) 当特征的数值范围差异很大时(比如房屋面积:0-2000平方米,卧室数量:1-5个),梯度下降会很慢并且可能震荡。特征归一化可以让梯度下降更快收敛!
为什么需要归一化? 想象一个椭圆形的等高线图 vs 圆形的等高线图,在圆形上梯度下降路径更直接!
两种常用的归一化方法 方法1:均值归一化(Mean Normalization)
其中:
= 特征j的均值
= 特征j的范围(max - min)
方法2:Z-score标准化(推荐!)
其中:
Python实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def normalize_features (X ): """特征归一化""" mu = np.mean(X[:, 1 :], axis=0 ) sigma = np.std(X[:, 1 :], axis=0 ) X_norm = X.copy() X_norm[:, 1 :] = (X[:, 1 :] - mu) / sigma return X_norm, mu, sigma from sklearn.preprocessing import StandardScalerscaler = StandardScaler() X_scaled = scaler.fit_transform(X[:, 1 :])
重要提醒 :
不要归一化偏置列 (全1的那一列)!
训练集的均值和标准差要保存下来,用于归一化测试集
预测时,新数据也要用同样的均值和标准差归一化
多变量梯度下降 梯度计算 对每个参数 :
更新规则 同时更新所有参数:
向量化实现 (强烈推荐!):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def gradient_descent_multi (X, y, theta, alpha, iterations ): """多变量梯度下降""" m = len (y) J_history = [] for i in range (iterations): predictions = X.dot(theta) errors = predictions - y theta = theta - alpha * (1 /m) * X.T.dot(errors) cost = np.sum (errors**2 ) / (2 *m) J_history.append(cost) return theta, J_history
学习率的选择 如何判断学习率是否合适? 绘制代价函数随迭代次数的变化图:
1 2 3 4 plt.plot(J_history) plt.xlabel('Iterations' ) plt.ylabel('Cost J' ) plt.title('Convergence Graph' )
正常收敛 :J逐渐减小并趋于平稳
学习率过大 :J可能震荡或发散(增大)
学习率过小 :收敛很慢
学习率的经验值 尝试这些值:…, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1, …(每次乘以3)
表格测试法选择最佳学习率(b站一个up主用的,但他说不是很重要,但我想学一下) 创建一个学习率表格,测试不同的值并绘图比较(AI写的代码就是冗长。。。):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 def test_learning_rates (X, y, iterations=100 ): """测试不同学习率的效果""" learning_rates = [0.001 , 0.003 , 0.01 , 0.03 , 0.1 , 0.3 , 1.0 ] results = [] fig, axes = plt.subplots(2 , 4 , figsize=(16 , 8 )) axes = axes.flatten() for idx, alpha in enumerate (learning_rates): theta = np.zeros(X.shape[1 ]) J_history = [] for i in range (iterations): predictions = X.dot(theta) errors = predictions - y theta = theta - alpha * (1 /len (y)) * X.T.dot(errors) cost = np.sum (errors**2 ) / (2 *len (y)) J_history.append(cost) results.append({ 'alpha' : alpha, 'final_cost' : J_history[-1 ], 'converged' : J_history[-1 ] < J_history[0 ], 'J_history' : J_history }) if idx < 8 : axes[idx].plot(J_history) axes[idx].set_title(f'α = {alpha} ' ) axes[idx].set_xlabel('Iteration' ) axes[idx].set_ylabel('Cost' ) axes[idx].grid(True ) if not results[-1 ]['converged' ]: axes[idx].set_facecolor('#ffcccc' ) elif J_history[-1 ] > 1e-2 : axes[idx].set_facecolor('#ffffcc' ) else : axes[idx].set_facecolor('#ccffcc' ) if len (learning_rates) < 8 : for idx in range (len (learning_rates), 8 ): axes[idx].set_visible(False ) plt.tight_layout() plt.suptitle('Learning Rate Comparison' , y=1.02 , fontsize=16 ) plt.show() print ("\n学习率测试结果表格:" ) print ("-" * 60 ) print (f"{'学习率' :<10 } {'最终代价' :<15 } {'是否收敛' :<10 } {'评价' :<15 } " ) print ("-" * 60 ) for result in results: alpha = result['alpha' ] final_cost = result['final_cost' ] converged = "✓" if result['converged' ] else "✗" if not result['converged' ] or final_cost > 1e10 : evaluation = "发散!过大" elif final_cost > 1e-1 : evaluation = "收敛太慢" elif final_cost < 1e-5 : evaluation = "很好!" else : evaluation = "可接受" print (f"{alpha:<10.3 f} {final_cost:<15.6 f} {converged:<10 } {evaluation:<15 } " ) print ("-" * 60 ) valid_results = [r for r in results if r['converged' ] and r['final_cost' ] < 1e10 ] if valid_results: best_result = min (valid_results, key=lambda x: x['final_cost' ]) print (f"\n推荐的最佳学习率: {best_result['alpha' ]} " ) return best_result['alpha' ] else : print ("\n警告:所有学习率都导致发散!需要更小的值。" ) return None best_alpha = test_learning_rates(X_norm, y, iterations=100 )
自适应学习率策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 def find_optimal_learning_rate (X, y, min_alpha=0.0001 , max_alpha=1.0 ): """二分查找最优学习率""" def test_alpha (alpha, iterations=50 ): """测试特定学习率是否收敛""" theta = np.zeros(X.shape[1 ]) prev_cost = float ('inf' ) for i in range (iterations): predictions = X.dot(theta) errors = predictions - y theta = theta - alpha * (1 /len (y)) * X.T.dot(errors) cost = np.sum (errors**2 ) / (2 *len (y)) if cost > prev_cost * 1.1 : return False , cost prev_cost = cost return True , cost left, right = min_alpha, max_alpha best_alpha = min_alpha while right - left > 0.0001 : mid = (left + right) / 2 converged, cost = test_alpha(mid) if converged: best_alpha = mid left = mid else : right = mid print (f"找到的最优学习率: {best_alpha:.4 f} " ) return best_alpha
学习率调试技巧总结
先用表格测试法 :快速了解合适的数量级
观察收敛图形 :
平滑下降→好
震荡但总体下降→学习率略大,可以减小
持续上升→学习率太大,必须减小
多特征时更要小心 :特征越多,通常需要更小的学习率
归一化后可用更大学习率 :特征归一化后,通常可以使用0.01-0.1的学习率
正规方程 Normal Equation 正规方程是通过解析方法一次性求解θ的方法,不需要迭代!
公式推导 通过令 ,可以得到:
Python实现 1 2 3 4 5 6 7 8 9 10 11 12 def normal_equation (X, y ): """使用正规方程求解theta""" theta = np.linalg.inv(X.T.dot(X)).dot(X.T).dot(y) theta = np.linalg.pinv(X.T.dot(X)).dot(X.T).dot(y) theta = np.linalg.lstsq(X, y, rcond=None )[0 ] return theta
梯度下降 vs 正规方程
梯度下降
正规方程
需要选择学习率α
不需要选择学习率
需要多次迭代
一次计算得出
当特征数量n很大时也能工作良好
需要计算 ,复杂度
适合所有类型的模型
只适用于线性模型
n > 10,000时推荐
n < 10,000时推荐
什么时候 不可逆?
特征之间线性相关(如:英尺和米同时作为面积特征)
特征数量>样本数量(n > m)
解决方法:
删除冗余特征
使用正则化(后面会学)
使用伪逆pinv而不是inv
多变量线性回归完整示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import pandas as pdimport numpy as npimport matplotlib.pyplot as pltdata = pd.read_csv('house_prices.csv' ) X = data[['size' , 'bedrooms' , 'age' ]].values y = data['price' ].values m = len (y) X_norm, mu, sigma = normalize_features(X) X_norm = np.column_stack([np.ones(m), X_norm]) n = X_norm.shape[1 ] theta = np.zeros(n) alpha = 0.01 iterations = 1500 theta_gd, J_history = gradient_descent_multi(X_norm, y, theta, alpha, iterations) X_with_bias = np.column_stack([np.ones(m), X]) theta_ne = normal_equation(X_with_bias, y) new_house = np.array([1650 , 3 , 10 ]) new_house_norm = (new_house - mu) / sigma new_house_norm = np.concatenate([[1 ], new_house_norm]) predicted_price = new_house_norm.dot(theta_gd) print (f"预测房价: ${predicted_price:,.2 f} " )
实践技巧总结
特征工程很重要 :可以添加多项式特征,如 , 等
一定要归一化 :不同scale的特征会让训练变得很困难
检查收敛 :画出J的变化图,确保在下降
保存归一化参数 :mu和sigma要保存,用于处理新数据
特征数量<1万用正规方程 :简单快速,不用调参
写在最后 这份笔记是我学习吴恩达机器学习课程第一次大作业的总结。我发现我好喜欢在iPad先写笔记,然后在Obsidian上补充写出来。
学习过程中的一些感悟
一开始连as是缩写的意思都不知道😂
plot函数总是记不住,每次都要复制粘贴
B站的up主们真的帮了很大忙
从(w,b)到θ向量的转换理解花了不少时间
矩阵乘法真的很优雅,一行代码搞定所有计算!
继续加油💪
—— 2025年,机器学习之旅刚刚开始