如何优雅地将二项逻辑斯蒂回归模型推广为多项逻辑斯蒂回归模型?
逻辑斯蒂回归本身只能用于二分类问题,如果实际情况是多分类的,那么就需要对模型进行一些改动,以下是三种比较常用的将逻辑斯蒂回归用于多分类的方法:
One vs One
OvO 的方法就是将多个类别中抽出来两个类别,然后将对应的样本输入到一个逻辑斯蒂回归的模型中,学到一个对这两个类别的分类器,然后重复以上的步骤,直到所有类别两两之间都存在一个分类器。
假设存在四个类别,那么分类器的数量为6个,表格如下:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 0 vs 1 | 0 vs 2 | 0 vs 3 | |
1 | 1 vs 2 | 1 vs 3 | ||
2 | 2 vs 3 | |||
3 |
分类器的数量直接使用 C^k_2 就可以了,k 代表类别的数量。
在预测时,需要运行每一个模型,然后记录每个分类器的预测结果,也就是每个分类器都进行一次投票,取获得票数最多的那个类别就是最终的多分类的结果。
比如在以上的例子中,6个分类器有3个投票给了类别3,1个投票给了类别2,1个投票给类别1,最后一个投票给类别0,那么久取类别3为最终预测结果。
OvO 的方法中,当需要预测的类别变得很多的时候,那么我们需要进行训练的分类器也变得很多了,这一方面提高了训练开销,但在另一方面,每一个训练器中,因为只需要输入两个类别对应的训练样本即可,这样就又减少了开销。
从预测的角度考虑,这种方式需要运行的分类器非常多,而无法降低每个分类器的预测时间复杂度,因此预测的开销较大。
以下是代码实现:
from sklearn.linear_model import LogisticRegression
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings('ignore')
def load_data():
train = pd.read_csv('D:/mnist/mnist_train.csv')
test = pd.read_csv('D:/mnist/mnist_test.csv')
columns = list(set(train.columns) - {'label'})
train[columns] = train[columns] / 255
test[columns] = test[columns] / 255
return train, test
def train_models(train):
models = {}
columns = list(set(train.columns) - {'label'})
# train models
for i in range(10):
for j in range(i + 1, 10):
cond = train['label'].isin([i, j]) # get train_data with labels in (i,j)
train_data = train[cond]
train_data_ij = train_data[columns] # train_data
train_label_ij = train_data[['label']] # train_label
lr = LogisticRegression(random_state=np.random.randint(0, 100)) # train model
lr.fit(train_data_ij,train_label_ij)
models['{}_{}'.format(i,j)] = lr # save models
return models
def predict_test(models, test):
result = []
# predict test data
for i in range(10):
for j in range(i + 1, 10):
model_name = '{}_{}'.format(i, j) # get model
predict = models[model_name].predict_proba(test[columns]) # predict
result.append(np.where(predict[:,0] > predict[:,1], i, j)) # save result
return result
def check_result(result):
result = np.vstack(result)
cnt = 0
# check each test data
for i in range(result.shape[1]):
p = Counter(result[:,i]).most_common(1)[0][0] # get the predict result with the highest number of vote
if p == test['label'].iloc[i]:
cnt += 1
acc = cnt / len(test) # calculate accuracy
return acc
train, test = load_data()
models = train_models(train)
result = predict_test(models, test)
acc = check_result(result)
print(acc)
# 0.9446
One vs All
OvA 的方法就是从所有类别中依次选择一个类别作为1,其他所有类别作为0,来训练分类器,因此分类器的数量要比 OvO 的数量少得多。
作为1的类别 | 作为0的类别 |
0 | 1;2;3 |
1 | 0;2;3 |
2 | 0;1;3 |
3 | 0;1;2 |
通过以上例子可以看到,分类器的数量实际上就是类别的数量,也就是k。
虽然分类器的数量下降了,但是对于每一个分类器来说,训练时需要将所有的训练数据全部输入进去进行训练,因此每一个分类器的训练时间复杂度是高于 OvO 的。
从预测的方面来说,因为分类器的数量较少,而每个分类器的预测时间复杂度不变,因此总体的预测时间复杂度小于 OvA。
预测结果的确定,是根据每个分类器对其对应的类别1的概率进行排序,选择概率最高的那个类别作为最终的预测类别。
以下是代码实现:
from sklearn.linear_model import LogisticRegression
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings('ignore')
def load_data():
train = pd.read_csv('D:/mnist/mnist_train.csv')
test = pd.read_csv('D:/mnist/mnist_test.csv')
columns = list(set(train.columns) - {'label'})
train[columns] = train[columns] / 255
test[columns] = test[columns] / 255
return train, test
def train_models(train):
models = {}
columns = set(train.columns) - {'label'}
# train models
for i in range(10):
# set the label == i to 1 and the others to 0
train_data = train.copy()
cond = train_data['label'] == i
train_data.loc[cond, 'label'] = 1
train_data.loc[~cond, 'label'] = 0
train_data_i = train_data[columns] # train_data
train_label_i = train_data[['label']] # train_label
lr = LogisticRegression(random_state=np.random.randint(0, 100)) # train model
lr.fit(train_data_i,train_label_i)
models[i] = lr # save models
return models
def predict_test(models, test):
result = []
columns = set(train.columns) - {'label'}
# predict test data
for i in range(10):
predict = models[i].predict_proba(test[columns]) # predict
result.append(predict[:,1]) # only save the result of class 1 is enough
return result
def check_result(result):
result = np.vstack(result)
cnt = 0
# check each test data
for i in range(result.shape[1]):
p = result[:,i]
p = np.where(p == p.max())[0][0] # select the predict class with the highest probability
if p == test['label'].iloc[i]:
cnt += 1
acc = cnt / len(test) # calculate accuracy
return acc
train, test = load_data()
models = train_models(train)
result = predict_test(models, test)
acc = check_result(result)
print(acc)
# 0.9201
从sigmoid函数到softmax函数的推导
第三种方式,我们可以直接从数学上使用 softmax 函数来得到最终的结果,而 softmax 函数与 sigmoid 函数有着密不可分的关系,它是 sigmoid 函数的更一般化的表示,而 sigmoid 函数是 softmax 函数的一个特殊情况。
我们知道 logit 函数代表的是某件事发生的几率,其形式为:
logit(P) = log\frac{P}{1-P} = wx\\
分子代表的是一件事发生的概率,分母代表这件事以外的事发生的概率,两者的和为1。
当我们面对的情况是多个分类时,可以让 k-1 个类别分别对剩下的那个类别做回归,即得到 k-1 个 logit 公式:
log\frac{P(Y=c_i|x)}{P(Y=c_k|x)} = w_ix (i=1,2,...,k-1)\\
然后对这些公式稍微变个型,可得:
P(Y=c_i|x) = P(Y=c_k|x)exp(w_ix) (i=1,2,...,k-1)\\
由于我们知道所有类别的可能性相加为1,因此可以得到:
P(Y=c_k|x) = 1-\sum_{i=1}^{k-1}P(Y=c_i|x)\\ = 1-\sum_{i=1}^{k-1}P(Y=c_k|x)exp(w_ix)\\
通过解上面的方程,可以得到关于某个样本被分类到类别 c_k 的概率:
P(Y=c_k|x) = 1-\sum_{i=1}^{k-1}P(Y=c_k|x)exp(w_ix)\\ P(Y=c_k|x)+\sum_{i=1}^{k-1}P(Y=c_k|x)exp(w_ix)=1\\ P(Y=c_k|x)+P(Y=c_k|x)\sum_{i=1}^{k-1}exp(w_ix)=1\\ P(Y=c_k|x)=\frac{1}{1+\sum_{i=1}^{k-1}exp(w_ix)}
这就是我们所了解的 softmax 函数了。
其他面试题解答:
面试题解答1:为什么线性回归要求假设因变量符合正态分布 - 知乎 (zhihu.com)
面试题解答2:各种回归模型与广义线性模型的关系 - 知乎 (zhihu.com)
面试题解答3:如何用方差膨胀因子判断多重共线性 - 知乎 (zhihu.com)
面试题解答4:逻辑斯蒂回归是否可以使用其他的函数替代 sigmoid 函数 - 知乎 (zhihu.com)
面试题解答5:特征存在多重共线性,有哪些解决方法? - 知乎 (zhihu.com)
面试题解答6:逻辑斯蒂回归为什么使用交叉熵而不是MSE - 知乎 (zhihu.com)
数据分析,机器学习社群正式启动~
需要学习资料,想要加入社群均可私信~
在这里我会不定期分享各种数据分析相关资源,技能学习技巧和经验等等~
详情私信,一起进步吧!
写于上海 2021-01-16