1. 先别急着写代码:2.2 linear到底在解决什么问题
很多学机器学习的朋友都会遇到这样一个场景:翻开某本教材或者打开某个课程的目录,第2.2节赫然写着"linear"(线性回归或线性模型)。讲义上的推导看了三遍,公式也抄下来了,但合上书想自己写一段能跑的代码,脑子里就一片空白。这个标题里的"2.2 linear代码带写",本质上就是解决这个"公式懂、代码不会"的断层问题。
我接触过不少初学者,面对linear这个知识点时最大的困惑不是"什么是线性回归",而是"我该怎么从零把它写出来"。查资料时搜到的示例代码,要么直接调sklearn一行搞定,要么就是一个几百行的完整工程文件,根本不知道哪些是核心、哪些是辅助。这种状态下的学习效率是很低的,因为代码一旦被封装起来,你对内部发生了什么就没有感知了。
所以我一直坚持一个观点:像linear这种基础模型,第一次接触时一定要亲手用NumPy从零实现一遍,而不是直接调工具库。这不是说工具库不好,而是说在入门阶段,手写一遍能让你把"模型表达式、损失函数、梯度下降"这条主线完完整整地经历一次。等你亲手把这条主线跑通了,以后看任何框架的线性回归实现,都会觉得非常轻松。
这篇文章就是一个"带写"向的教程。我会带着你从数据准备开始,一行一行把线性回归的核心代码写出来。全程只用NumPy,不调sklearn里的LinearRegression,不调PyTorch或TensorFlow的线性层,就靠python的基础语法和矩阵运算把整个训练流程跑通。文章里每一段代码我都会说明"这一行在干什么""为什么必须这么写""少了它会出什么问题"。
这个教程适合谁?一是正在学机器学习基础、被各种公式搞得头晕的初学者,二是已经会用调库但想弄清内部原理的开发者,三是准备面试、需要手推手写线性模型的求职者。看完之后,你不仅能跑出一个收敛的线性回归模型,还能自己画出损失曲线、验证梯度正确性,甚至顺着这个思路去理解后面的逻辑回归和神经网络。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前的三个关键选择:数据、模型与损失函数
写代码之前,必须先把三件事想清楚:用什么样的数据来实验、用什么样的模型表达方式来写、用什么样的损失函数来度量好坏。这三件事决定了代码的整体结构,很多人在这一步草草略过,直接就开始写训练循环,结果后面调试的时候到处出问题。
2.1 准备一份"能讲清楚问题"的实验数据
我见过太多人上来就用UCI或者Kaggle的真实数据集练手。真实数据集当然好,但对于一个刚学线性回归的人来说,它有两个致命的问题:一是数据分布未知,你不知道理想的拟合结果长什么样;二是特征维度多、量纲差异大,一上来就要处理标准化、缺失值之类的麻烦事。所以我建议,带写阶段的第一份数据,应该自己生成。
这里我们用最简单的单变量线性数据:自变量x从-1到1均匀取100个数,对应的y由一条直线加上高斯噪声生成。噪声的作用是模拟真实场景中的随机干扰,让数据不是完美落在直线上,否则我们的模型几乎不需要学习。
python复制import numpy as np
# 固定随机种子,保证每次运行结果一致
np.random.seed(42)
# 生成100个均匀分布的x,范围[-1, 1]
x = np.linspace(-1, 1, 100)
# 真实参数:w = 2.0, b = 1.0
true_w = 2.0
true_b = 1.0
# y = 2x + 1 + 高斯噪声(标准差0.2)
y = true_w * x + true_b + np.random.normal(0, 0.2, size=x.shape)
# 转化成列向量,方便矩阵运算
x = x.reshape(-1, 1)
y = y.reshape(-1, 1)
print(x.shape, y.shape) # (100, 1) (100, 1)
这里有一个很多新手会忽略的细节:为什么要reshape成列向量?因为后面我们要做矩阵乘法,NumPy对一维数组和二维矩阵的广播规则不一样。如果x保持一维数组,x.shape是(100,),x和某个(2,1)形状的矩阵相乘时,广播规则会自动给你补维度,但补出来的形状往往不是你想要的那个。统一reshape成(100,1)的列向量,后续所有矩阵运算的维度关系就清晰了。
固定随机种子(seed)也是一个好习惯。噪声是随机生成的,不固定种子的话,你每次运行得到的数据都不一样,复现实验结果的时候就说不清楚了。设了seed之后,无论你跑多少次,生成的x和y都是一份固定的数据。
2.2 模型表达式的两种写法:循环求和还是矩阵运算
线性回归的模型表达式很简单:f(x) = w*x + b。在代码里写出来有两种方式。
第一种是标量循环的方式,把每个样本单独拿出来算一次预测值:
python复制# 初始参数
w = 0.0
b = 0.0
def predict_scalar(x, w, b):
return w * x + b
这种方式直观,但循环速度慢,而且当特征维度多的时候,代码会变得很长。更关键的是,它和后面梯度下降的向量化更新方式不好衔接。
第二种是矩阵运算的方式,这也是我们实际要用的:
python复制# 把参数矩阵化:W为一维向量,这里只有一个特征
W = np.zeros((1, 1)) # 形状(1,1)
b = np.zeros((1,)) # 形状(1,)
def predict(X, W, b):
return X @ W + b
这里的X形状是(100,1),W形状是(1,1),X @ W的结果是(100,1),也就是每个样本的预
