AI 学习之路(四)- 逻辑回归问题

73 阅读16分钟

逻辑回归需要更为扎实的数学基础才能理解其原理。我看了很多资料,还是不得其要义,把我的所思所想记录下来,给和我一样的小白看看逻辑回归到底是怎么回事。

1. 什么是逻辑回归问题

首先逻辑回归的结果是2分的,而不是线性的,这比较好理解。

线性问题:

  • 房子面积越大,价格越高
  • 现在数据:50平米500万元,60平米600万元
  • 请预测:70平米多少钱

2分问题:

  • 学习越认真越不会挂科

    • 指标1:课堂出勤率
    • 指标2:作业完成率
  • 现在数据:

学生ID课堂出勤率作业完成率是否挂科
10.950.980
20.650.700
30.90.40
40.350.401
50.850.251
60.300.801
  • 请预测:出勤率80%,作业完成率40%,是否会挂科

2. 如何解决逻辑回归问题

数学的方式有点难,懂得朋友可能也不太需要看这个文章。只能介绍 python 里如何解决这个问题,仅仅是知其然,不知其所以然。

其实也能知道,计算方法是以一个学习率+一个公式去不停的计算斜率与特征权重,就是不知道为什么可以这么算。为什么这么算可以定义出导数的次方。

import numpy as np
from sklearn.linear_model import LogisticRegression

# 训练数据(与简化版数据集一致)
X_train = np.array([
    [0.95, 0.98],  # 学生1:未挂科
    [0.65, 0.70],  # 学生2:未挂科
    [0.9, 0.4],  # 学生3:未挂科
    [0.35, 0.40],  # 学生4:挂科
    [0.85, 0.25],  # 学生5:挂科
    [0.30, 0.80]   # 学生6:挂科
])
y_train = np.array([0, 0, 0, 1, 1, 1])

# 训练逻辑回归模型
model = LogisticRegression()
model.fit(X_train, y_train)

# 新样本预测:出勤率80%(0.8),作业提交率40%(0.40)
new_student = np.array([[0.8, 0.4]])

# 预测概率和类别
prob = model.predict_proba(new_student)[:, 1]  # 挂科概率
pred = model.predict(new_student)              # 预测类别(0/1)

# 输出结果
print(f"挂科概率: {prob[0]:.2%}")  # 示例输出: 49.84%
print(f"预测结果: {'挂科' if pred[0] == 1 else '未挂科'}")  # 示例输出: 未挂科

3. 逻辑回归的数据集

在自我尝试中,发现逻辑回归极易被干扰,比如异常值和数据分布都会对预测造成很大的影响。

异常值:比如数据中有一条 0.9,0.9,1 的数据,明明是双高但是挂科了,这种数据会严重干扰回归测试。

数据分布:如果数据中不挂科的只有2条,挂科的有3条,这种数据也会导致最后的概率偏向挂科

所以当我们决定采用逻辑回归算法的时候,需要提前确认数据集是否符合标准。

DeepSeek 给出了一种不错的办法,就是将数据在二维上呈现出来。

import numpy as np
from sklearn.linear_model import LogisticRegression

# 训练数据(与简化版数据集一致)
X_train = np.array([
    [0.95, 0.98],  # 学生1:未挂科
    [0.65, 0.70],  # 学生2:未挂科
    [0.9, 0.4],  # 学生3:未挂科
    [0.35, 0.40],  # 学生3:挂科
    [0.85, 0.25],  # 学生4:挂科
    [0.30, 0.80]  # 学生5:挂科
])
y_train = np.array([0, 0, 0, 1, 1, 1])

import matplotlib.pyplot as plt

plt.scatter(X_train[y_train == 0, 0], X_train[y_train == 0, 1], label='Not Fail', marker='o')
plt.scatter(X_train[y_train == 1, 0], X_train[y_train == 1, 1], label='Fail', marker='x')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend()
plt.title('Student Data')
plt.show()

  • 从图中可以看出,点位可以用一条线轻易的隔开,说明数据符合线性可分性

  • 点位数量一致,所以结果是均匀分布的,可以用0.5作为判断分类的阈值

相反的,如果点位你中有我,我中有你,则不适用逻辑回归,如果有个别点位,在对方阵营中,则需要先剔除掉。

如果点位数量,一种特别多,另一种特别少,则需要重新采样,或提高数据少的权重,否则逻辑回归也将不准确

5. 逻辑回归解题的基本思路

上面这个例子数据量太少,又全被训练了,所以评估以下指标(准确率、精确率、召回率、F1分数、AUC-ROC),模型的评估指标均为1。

我们拿 kaggle 挑战赛里的泰坦尼克号幸存者为例,我们看看一个简单的逻辑回归过程必要的步骤

在这个问题中,研究人员收集了泰坦尼克号乘客的多维度数据。这些数据涵盖乘客的基本信息,比如年龄、性别;社会经济信息,像船舱等级(一等舱、二等舱、三等舱);家庭结构信息,包括兄弟姐妹及配偶数量(SibSp)、父母及子女数量(Parch);还有船票价格(Fare)、登船港口(Embarked)等。

我们需要对这些指标做逻辑回归,最后得到一个预测是否可以幸存的模型。

import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score

# 读取数据
# 从CSV文件中读取训练集数据,该数据包含了乘客的各种信息以及是否幸存的标签
train = pd.read_csv('./train.csv')
# 从CSV文件中读取测试集数据,该数据只包含乘客信息,需要预测是否幸存
test = pd.read_csv('./test.csv')

# 数据预处理函数
def preprocess(df):
    # 选择需要使用的特征列
    features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
    # 从数据集中提取选择的特征列
    df = df[features]

    # 处理缺失值
    # 使用年龄的中位数填充年龄列中的缺失值
    df.loc[:, 'Age'] = df['Age'].fillna(df['Age'].median())
    # 使用票价的中位数填充票价列中的缺失值
    df.loc[:, 'Fare'] = df['Fare'].fillna(df['Fare'].median())
    # 使用登船港口的众数填充登船港口列中的缺失值
    df.loc[:, 'Embarked'] = df['Embarked'].fillna(df['Embarked'].mode()[0])

    # 对分类变量进行独热编码
    # 将性别和登船港口这两列分类变量转换为数值型的独热编码格式,方便模型处理
    df = pd.get_dummies(df, columns=['Sex', 'Embarked'])

    return df

# 对训练集进行预处理,得到特征矩阵
X_train = preprocess(train)
# 从训练集中提取目标变量(是否幸存)
y_train = train['Survived']

# 对测试集进行预处理,得到特征矩阵
X_test = preprocess(test)

# 划分训练集和验证集
# 将训练集按80:20的比例划分为新的训练集和验证集,random_state设置为42保证每次划分结果一致
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2, random_state=42)

# 初始化并训练模型
# 初始化逻辑回归模型,使用liblinear求解器,设置class_weight='balanced'来处理类别不平衡问题,最大迭代次数设置为1000
model = LogisticRegression(solver='liblinear', class_weight='balanced', max_iter=1000)
# 使用新的训练集数据对模型进行训练
model.fit(X_train, y_train)

# 验证评估
# 使用训练好的模型对验证集进行预测,得到预测结果
y_val_pred = model.predict(X_val)
# 计算验证集的准确率,即预测正确的样本数占总样本数的比例
val_accuracy = accuracy_score(y_val, y_val_pred)
# 计算验证集的精确率,即预测为正类的样本中实际为正类的比例
val_precision = precision_score(y_val, y_val_pred)
# 计算验证集的召回率,即实际为正类的样本中被预测为正类的比例
val_recall = recall_score(y_val, y_val_pred)
# 计算验证集的F1分数,它是精确率和召回率的调和平均数
val_f1 = f1_score(y_val, y_val_pred)
# 计算验证集的AUC-ROC值,它衡量了模型的排序能力
val_auc = roc_auc_score(y_val, y_val_pred)
# 打印验证集的各项评估指标
print(f"Validation Accuracy: {val_accuracy:.4f}")
print(f"Validation Precision: {val_precision:.4f}")
print(f"Validation Recall: {val_recall:.4f}")
print(f"Validation F1 Score: {val_f1:.4f}")
print(f"Validation AUC-ROC: {val_auc:.4f}")

# 预测测试集并生成提交文件
# 使用训练好的模型对测试集进行预测,得到预测结果
test_predictions = model.predict(X_test)

# 创建一个DataFrame用于存储提交结果
submission = pd.DataFrame({
    'PassengerId': test['PassengerId'],  # 乘客ID
    'Survived': test_predictions  # 预测的是否幸存结果
})
# 将提交结果保存为CSV文件,不保存行索引
submission.to_csv('./gender_submission.csv', index=False)
  • 数据读取:从CSV文件中加载训练集和测试集。

  • 数据预处理

    • 选择特征:选择了Pclass, Sex, Age, SibSp, Parch, Fare, Embarked等特征。
    • 处理缺失值:用中位数填充AgeFare的缺失值,用众数填充Embarked的缺失值。
    • 独热编码:将分类变量SexEmbarked转换为独热编码(One-Hot Encoding)。
  • 划分训练集和验证集:将训练集进一步划分为训练集和验证集,用于模型评估。

  • 模型训练:使用逻辑回归模型进行训练,设置了solver='liblinear'class_weight='balanced'来处理类别不平衡问题。

  • 模型评估:在验证集上计算准确率、精确率、召回率、F1分数和AUC-ROC等指标。

  • 生成提交文件:对测试集进行预测,并生成提交文件

这个版本在 kaggle 的排行榜上,以0.74的分数得到了 10000+ 的成绩

6. 逻辑回归解题的优化思路

10000多名肯定是做的太草率了,所以我们需要对上面的代码做优化,有以下常见的优化方式:添加新特征,超参数优化(网格搜索)

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
from sklearn.preprocessing import StandardScaler

# 读取数据
# 从CSV文件中读取训练数据,训练数据包含特征和目标变量(是否幸存)
train = pd.read_csv('./train.csv')
# 从CSV文件中读取测试数据,测试数据仅包含特征,需要预测目标变量
test = pd.read_csv('./test.csv')

# 数据预处理函数,用于处理数据中的缺失值、添加新特征和进行独热编码
def preprocess(df):
    # 选择要使用的特征列
    features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
    # 复制数据,避免修改原始数据
    df = df[features].copy()

    # 处理缺失值
    # 使用年龄的中位数填充年龄列中的缺失值
    df.loc[:, 'Age'] = df['Age'].fillna(df['Age'].median())
    # 使用票价的中位数填充票价列中的缺失值
    df.loc[:, 'Fare'] = df['Fare'].fillna(df['Fare'].median())
    # 使用登船港口的众数填充登船港口列中的缺失值
    df.loc[:, 'Embarked'] = df['Embarked'].fillna(df['Embarked'].mode()[0])

    # 添加新特征
    # 判断乘客是否为儿童(年龄小于12岁),并将结果转换为整数类型
    df.loc[:, 'IsChild'] = (df['Age'] < 12).astype(int)
    # 判断乘客是否为女性,并将结果转换为整数类型
    df.loc[:, 'IsFemale'] = (df['Sex'] == 'female').astype(int)
    # 计算家庭成员数量,包括兄弟姐妹、配偶、父母、子女和乘客本人
    df.loc[:, 'FamilySize'] = df['SibSp'] + df['Parch'] + 1
    # 判断乘客是否独自一人旅行(家庭成员数量为1),并将结果转换为整数类型
    df.loc[:, 'IsAlone'] = (df['FamilySize'] == 1).astype(int)

    # 对 'Sex' 和 'Embarked' 列进行独热编码
    # 将分类变量转换为数值变量,以便模型能够处理
    df = pd.get_dummies(df, columns=['Sex', 'Embarked'])

    return df

# 对训练集和测试集进行预处理
# 对训练集进行预处理,得到特征矩阵
X_train = preprocess(train)
# 从训练集中提取目标变量(是否幸存)
y_train = train['Survived']
# 对测试集进行预处理,得到特征矩阵
X_test = preprocess(test)

# 特征缩放
# 初始化标准化缩放器,用于将特征缩放到相同的尺度
scaler = StandardScaler()
# 对训练集进行拟合和转换,得到缩放后的训练集特征矩阵
X_train_scaled = scaler.fit_transform(X_train)
# 使用训练集的缩放参数对测试集进行转换,得到缩放后的测试集特征矩阵
X_test_scaled = scaler.transform(X_test)

# 初始化逻辑回归模型并进行超参数调优
# 定义超参数网格,包含要尝试的不同超参数组合
param_grid = {
    'C': [0.001, 0.01, 0.1, 1, 10],  # 正则化强度的倒数,值越小正则化越强
    'solver': ['liblinear', 'lbfgs'],  # 求解器类型
    'class_weight': [None, 'balanced']  # 类别权重,用于处理类别不平衡问题
}
# 初始化逻辑回归模型,设置最大迭代次数为1000
model = LogisticRegression(max_iter=1000)
# 初始化网格搜索对象,使用5折交叉验证和准确率作为评估指标
grid_search = GridSearchCV(model, param_grid, cv=5, scoring='accuracy')
# 在缩放后的训练集上进行网格搜索,找到最佳超参数组合
grid_search.fit(X_train_scaled, y_train)

# 获取最佳模型
best_model = grid_search.best_estimator_

# 验证评估
# 使用5折交叉验证对最佳模型进行评估,得到交叉验证的准确率分数
cv_scores = cross_val_score(best_model, X_train_scaled, y_train, cv=5)
# 打印交叉验证的平均准确率
print(f"Cross-validation Accuracy: {np.mean(cv_scores):.4f}")

# 在验证集上评估
# 将缩放后的训练集进一步划分为训练集和验证集,验证集占比20%
X_train_split, X_val, y_train_split, y_val = train_test_split(X_train_scaled, y_train, test_size=0.2, random_state=42)
# 使用训练集对最佳模型进行训练
best_model.fit(X_train_split, y_train_split)
# 使用最佳模型对验证集进行预测,得到预测结果
y_val_pred = best_model.predict(X_val)

# 计算验证集的评估指标
# 计算准确率,即预测正确的样本数占总样本数的比例
val_accuracy = accuracy_score(y_val, y_val_pred)
# 计算精确率,即预测为正类的样本中实际为正类的比例
val_precision = precision_score(y_val, y_val_pred)
# 计算召回率,即实际为正类的样本中被预测为正类的比例
val_recall = recall_score(y_val, y_val_pred)
# 计算F1分数,它是精确率和召回率的调和平均数
val_f1 = f1_score(y_val, y_val_pred)
# 计算AUC-ROC值,它衡量了模型的排序能力
val_auc = roc_auc_score(y_val, y_val_pred)

# 打印验证集的评估指标
print(f"Validation Accuracy: {val_accuracy:.4f}")
print(f"Validation Precision: {val_precision:.4f}")
print(f"Validation Recall: {val_recall:.4f}")
print(f"Validation F1 Score: {val_f1:.4f}")
print(f"Validation AUC-ROC: {val_auc:.4f}")

# 预测测试集并生成提交文件
# 使用最佳模型对缩放后的测试集进行预测,得到预测结果
test_predictions = best_model.predict(X_test_scaled)

# 创建一个DataFrame,包含乘客ID和预测的生存结果
submission = pd.DataFrame({
    'PassengerId': test['PassengerId'],
    'Survived': test_predictions
})
# 将预测结果保存为CSV文件,不包含行索引
submission.to_csv('./gender_submission.csv', index=False)
  • 添加新特征

    • IsChild:判断乘客是否为儿童(年龄小于12岁)。
    • IsFemale:判断乘客是否为女性。
    • FamilySize:家庭成员数量(兄弟姐妹/配偶 + 父母/子女 + 本人)。
    • IsAlone:判断乘客是否独自一人旅行。

这些新特征有助于捕捉更多与生存相关的潜在信息。

  • 超参数 调优:通过GridSearchCV进行网格搜索,自动寻找最佳的超参数组合。具体调整的超参数包括:

    • scoring: 针对 accuracy 准确率进行优化

经过这个版本的优化,以0.78的成绩来到了排行榜 2000+ 的水平

7. 逻辑回归在实际中的应用

实际中,逻辑回归常处理多分类问题,像新闻文本就可被分为政治、经济、体育等类别。多分类基础上的多标签拓展,能让内容标注更精准。

以新闻为例,传统多分类下一篇新闻只能归为一类,但现实里一篇新闻可能同时涉及多个领域。多标签逻辑回归为每个可能的标签,如政治、经济、科技等,分别建立独立的逻辑回归模型。训练时,提取新闻文本的关键词、主题等特征,模型会依据这些特征计算文本属于每个标签的概率。比如一篇新闻,既提到 “企业并购”,又涉及 “5G 技术应用”,经济和科技标签对应的模型就会算出较高概率。

预测新文本时,各个标签模型同时运算,输出概率值。设定一个概率阈值,如 0.5,高于它就判定文本有这个标签。这样,新闻就能被同时打上多个相关标签。

所以逻辑回归算法对于平台型公司来说是至关重要的。

为什么这么说呢,假设你是一位歌手,你发行了一张唱片,对于你自己而言,唱片里的每首歌曲都是什么曲风是一目了然的,就算是要一一标注,也是很简单的。如果你做一个个人独立站,那把这些内容更新上去轻而易举。

但假设你是一个音乐平台(如网易云),里同时有10w~100w+创作者,他们每天产出的作品就是海量的,平台如果要通过一首一首听完,再给每首歌曲打上诸如(摇滚、rap、流行)等标签,那就太费时费力了。

假设我们对存量的音乐的文本,节奏进行采样后,利用逻辑回归来训练一个识别音乐标签的模型,这样就可以给使用平台的用户听到最新的他们可能感兴趣的内容。

写到这突然恍然大悟,抖音,曾今令人叫好的算法推荐,到如今令人诟病的信息茧房,其中不也蕴含了一些逻辑回归的道理吗。只不过他们在这个基础上又加入了深度学习等复杂的算法,才在2016 - 2020这个时间内,赶超各大同行,成为了一个巨无霸。难怪张一鸣曾说类似能交给算法的就不要交给人工来解决的话。

8. 总结

本文围绕逻辑回归展开,先是区分线性问题与逻辑回归二分问题,以房子价格和学生挂科情况为例,让读者理解逻辑回归应用场景。随后通过 Python 代码展示解决逻辑回归问题的方法,包括模型训练、预测以及数据可视化检查数据集等。

接着以泰坦尼克号幸存者预测为案例,详细呈现数据读取、预处理(选特征、补缺失值、独热编码)、划分训练验证集、模型训练与评估、生成提交文件的全过程。之后阐述优化思路,如添加新特征(IsChild、IsFemale 等)和超参数调优(GridSearchCV)。

最后,文章介绍逻辑回归在音乐平台歌曲标签识别和抖音算法推荐等实际场景中的应用,展示其重要性与广泛应用。

如果你觉得这篇文章对你理解逻辑回归有所帮助,无论是对概念的讲解,还是代码示例,亦或是对实际应用场景的介绍,欢迎点赞❤️收藏⭐️支持!你的点赞是对我创作的最大鼓励,也能帮助更多人看到这篇文章,让我们一起学习进步!

下期预告:AI 学习之路(五)- 决策树能决策什么