logistics regression
P与NP
P:能在多项式时间能找到解;
NP:能在多项式时间内验证候选解的对错。
NP 难:所有 NP 问题都能在多项式时间内归约到它,即,它至少和 NP 类问题一样难。不要求多项式时间可验证。
NP 完全:NP ∩ NP 难
机器学习的很多问题是 NP 难甚至更难的问题。但学习算法必须在多项式时间内找到解。如果可以彻底避免过拟合,那就可以通过优化经验误差得到最优解,构造性证明了 “P = NP”。因此,如果我们相信 “P ≠ NP”,过拟合就不可避免。
留出法
使用注意事项
(1)分层抽样。训练集、测试集中样本类别比例需和原数据集保持一致;
(2)train ratio。如果训练样本过小,结果偏差大。如果测试样本过小,结果方差大。
代码
Logistic Regression
数学原理
- 为什么需要 Logistic Regression? 答:Logistic Regression 本质上是解决分类问题。如果使用单位阶跃函数输出标签,再用 MSE (均方误差)进行拟合,由于单位阶跃函数不可导,从数学上讲极难优化。
- Logistic Regression 做了什么去完成分类?答:使用 sgmoid 函数讲线性模型的输出值由离散标签变成了连续的概率值,即模型输出为正例的概率;构造负似然对数求解分类正确的最优参数;sgmoid 函数引入了非线性,没有闭式解(可以直接通过数学公式等方法计算出的解),可通过牛顿法求最优解。
负似然对数及牛顿法求解
似然函数:likelihood=∑i=1mp0y0p1y1似然函数:likelihood = \sum_{i=1}^{m}p_{0}^{y_{0}}p_{1}^{y_{1}}似然函数:likelihood=∑i=1mp0y0p1y1
负对数似然:negative_log_likelihood=−∑i=1m(y0logp0+y1logp1)=∑i=1mlog(1+ezi)−yizi负对数似然:negative\_ log\_likelihood = -\sum_{i=1}^{m}(y_{0}logp_{0}+y_{1}logp_{1}) =\sum_{i=1}^{m}log(1+e^{z_{i}})-y_{i}z_{i}负对数似然:negative_log_likelihood=−∑i=1m(y0logp0+y1logp1)=∑i=1mlog(1+ezi)−yizi
牛顿法:coefficientst+1=coefficientst−H−1grad牛顿法:coefficients_{t+1} =coefficients_{t}-H^{-1}grad牛顿法:coefficientst+1=coefficientst−H−1grad
二阶导(H):Hjk=∑i=1mpi(1−pi)xijxik,j/k是参数编号Hjk是矩阵里的一个元素H_{jk} = \sum_{i=1}^{m} p_i(1-p_i)x_{ij}x_{ik},j/k是参数编号 H_{j}{k}是矩阵里的一个元素Hjk=∑i=1mpi(1−pi)xijxik,j/k是参数编号Hjk是矩阵里的一个元素
梯度(grad)=∑i=1m(pi−yi)xi梯度(grad) =\sum_{i=1}^{m}(p_{i}-y_{i})x_{i}梯度(grad)=∑i=1m(pi−yi)xi
代码
@dataclass# 快速定义只设置数据的类,比如 initclassTrainingResult:coefficients:list[float]loss:floatiterations:intconverged:bool# 是否收敛classLogisticRegression:def__init__(self,max_iter:int=100,tolerance:float=1e-8,l2:float=0.01)->None:# l2 = 0.01ifmax_iter<=0:raiseValueError("max_iter must be positive.")iftolerance<=0:raiseValueError("tolerance must be positive.")ifl2<0:raiseValueError("l2 must be non-negative.")self.max_iter=max_iter self.tolerance=tolerance self.l2=l2 self.coefficients:list[float]=[]deffit(self,features:list[list[float]],labels:list[int])->TrainingResult:ifnotfeatures:raiseValueError("No training data was provided.")# 特征值非空iflen(features)!=len(labels):raiseValueError("Feature rows and labels must have the same length.")# 特征值和标签一一对应ifset(labels)-{0,1}:# 对二分类而言,只有 0,1raiseValueError("Labels must be encoded as 0 or 1.")feature_count=len(features[0])# 取出特征值的属性个数ifany(len(row)!=feature_countforrowinfeatures):# 每个特征的属性个数必须一样,不多不少raiseValueError("All feature rows must have the same length.")design_matrix=[[1.0]+rowforrowinfeatures]# 增加截距项parameter_count=feature_count+1# 参数值+1coefficients=[0.0]*parameter_count# 初始化参数列表converged=False# 未收敛foriterationinrange(1,self.max_iter+1):# 最多迭代 max_iter 次probabilities=[sigmoid(dot(coefficients,row))forrowindesign_matrix]# 预测概率值列表gradient=[0.0]*parameter_count# 梯度初始化hessian=[[0.0]*parameter_countfor_inrange(parameter_count)]# 二阶导hessian矩阵forrow,probability,labelinzip(design_matrix,probabilities,labels):# 对于每一组数据residual=probability-label weight=probability*(1.0-probability)forcol_indexinrange(parameter_count):gradient[col_index]+=residual*row[col_index]# 按照公式,不断累加梯度forinner_indexinrange(parameter_count):hessian[col_index][inner_index]+=weight*row[col_index]*row[inner_index]# 累加 hessian 矩阵中的每个元素forindexinrange(1,parameter_count):# 在截距以外的其他参数加正则化项,下面是求一阶导 二阶导之后的内容:gradient[index]+=self.l2*coefficients[index]hessian[index][index]+=self.l2# m 个样本之后才更新step=solve_linear_system(hessian,gradient)coefficients=[coefficient-deltaforcoefficient,deltainzip(coefficients,step)]ifvector_norm(step)<self.tolerance:converged=Truebreakself.coefficients=coefficientsreturnTrainingResult(coefficients,self.loss(features,labels),iteration,converged)defpredict_proba_one(self,feature_row:list[float])->float:ifnotself.coefficients:raiseValueError("Model has not been fitted yet.")returnsigmoid(self.coefficients[0]+dot(self.coefficients[1:],feature_row))defpredict_one(self,feature_row:list[float],threshold:float=0.5)->int:returnint(self.predict_proba_one(feature_row)>=threshold)defloss(self,features:list[list[float]],labels:list[int])->float:design_matrix=[[1.0]+rowforrowinfeatures]negative_log_likelihood=0.0forrow,labelinzip(design_matrix,labels):score=dot(self.coefficients,row)negative_log_likelihood+=softplus(score)-label*score regularization=0.5*self.l2*sum(coefficient*coefficientforcoefficientinself.coefficients[1:])returnnegative_log_likelihood+regularization读写文件(csv/txt/json/md)
importcsv# 读取和写入 CSV 文件frompathlibimportPath# 更方便、安全地处理文件路径path=Path("data.txt")DEFAULT_DATA_PATH=Path(__file__).with_name("watermelon_3a.csv")# _file_: 当前 Python 文件的路径字符串;with_name: 要找的文件名- 前者是相对于当前工作目录读取名为data.txt的文件,返回的是一个 Path 对象;
- 后者是相对于当前 .py 脚本读取名为 watermelon_3a.csv 的文件,返回的是一个 Path 对象。
列表推导式的三种形式
列表推导式就是一种快速生成列表的写法。
missing_columns=[columnforcolumnin(*feature_columns,label_column)ifcolumnnotinreader.fieldnames]# 检查特征列和标签列是否存在ifmissing_columns:raiseValueError(f"Missing columns in CSV:{', '.join(missing_columns)}")[表达式for变量in可迭代对象][表达式for变量in可迭代对象if条件][值1if条件else值2for变量in可迭代对象]# if(){值1}else{值2}- *后面加元组、列表、字符串、range、集合、字典表示解包。默认情况下,解包字典的key。如果需要解包值/键值对: *dict.values()/ *dict.items()
- 遍历特征列和标签列,如果有不在表头的,就记录在列表 missing_columns。
- 分隔符.join(字符串列表):在每两个元素之间放一个分隔符,用来把一堆字符串拼成一个字符串。比如:
parts=["2026","08","13"]print("-".join(parts))# 2026-08-13- python里的条件表达式,三元表达式,比如
Aif条件elseB1ifrow[label_column].strip()==positive_labelelse0- with···as···:,用法如下
with表达式as变量:代码块# Python 的上下文管理器语法# 最常见用途是打开文件后自动关闭文件withpath.open("r",encoding="utf-8")asf:text=f.read()withpath.open("r",encoding="utf-8-sig",newline="")ascsv_file:- 把文件打开,把打开的文件对象命名为 csv_file;
- 在 with 代码块里使用它
- 代码块结束后,自动关闭文件
- newline=“”:它控制 Python 打开文本文件时怎么处理换行符。CSV 模块自己会处理换行。如果你不写 newline=“”,Python 可能先自动转换一遍换行,csv 模块又处理一遍,尤其在 Windows 上写 CSV 时,可能出现多余空行。
- “utf-8-sig”:用 UTF-8 读取,同时兼容文件开头可能存在的 BOM。
withpath.open("r",encoding="utf-8-sig",newline="")ascsv_file:# new_linereader=csv.DictReader(csv_file)# 一个读取器。这个读取器有自己的读取规则ifreader.fieldnamesisNone:# 读取器(相当于是一个类).fieldnames表示取出表头,其实也就是字典的键,列表raiseValueError("CSV file has no header row.")- 创建了一个对象(DictReader):可迭代、内部有自己的读取规则
- 它的规则:读取第一行作为表头 fieldnames;后面每一行数据(value),都和表头(key)配对,每次迭代返回一个字典 row。
- 也可以这么理解,csv.DictReader 是一个类,csv.DictReader(csv_file) 创建一个 DictReader 实例对象。
row_ids:list[str]=[]# 保存样本编号features:list[list[float]]=[]# 保存样本特征矩阵labels:list[int]=[]# 保存标签,正类为 1,负类为 0forline_number,rowinenumerate(reader,start=2):# reader是一个可迭代对象,enumerate是给它加编号从1开始但是这里规定从2开始。enumerate 返回的对象是什么?编号加在最前面?row_ids.append(row.get("编号",str(line_number-1)))# 是从可迭代对象中读出的字典.dict.get(key,default)如果dict[key]存在那么就用它;否则就用行编号减1features.append([float(row[column])forcolumninfeature_columns])# 添加的元素添加的是一个列表。列表中的元素就是字典从键当中取的值labels.append(1ifrow[label_column].strip()==positive_labelelse0)# python中的三元推导式- enumerate() 返回的是一个可迭代对象,它每次迭代会返回一个二元组:(编号, 原来的元素);指定起始编号(start=2),默认从0开始。
- enumerate() 不是把编号真的“加进”原列表或原字典里,它只是遍历时临时配一个编号。
returnrow_ids,list(feature_columns),features,labels- 注意!有逗号才是元组,括号只是分组或者提高可读性。当然类型打印的时候,括号也是用来区分类别的一个标志。但自己写的时候,有括号,声明的就是元组。比如这个 return 返回的就是一个元组。
