Scott's Blog

学则不固, 知则不惑

0%

监督学习实例:学校图书数据分类

我们学校比旁边的学校在教科书上面花了更多的钱吗?这是否有用呢?

问题

学校想知道,我们比旁边的学校在教科书上面花了更多的钱吗?这是否有用?

要回答这个问题,首先我们需要有教科书的数据,并进行分类。然而分类是一个及其复杂的操作,学校每年都会花很多的时间去手动分类。

我们的目标是可以建立一个机器学习模型自动处理这个分类步骤。

比如《线性代数》,这本书我们会给他几个标签:

  • 数学
  • 教科书
  • 中学

这些标签,就是我们的target variable。

这是一个典型的分类问题。

不过我们的预测应该由概率来定义,我们不会预测说,这就是本数学书,而是说,我有百分之60的把握认为这是一本数学书,如果不对,那我有百分之70的把握认为这是一本物理书。

导入数据

数据是csv格式的,我们使用df = pd.read_csv('TrainingData.csv')导入数据并保存到名为df的变量。

探索数据

基本信息

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
In [6]: df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1560 entries, 0 to 1559
Data columns (total 26 columns):
Unnamed: 0 1560 non-null int64
Function 1560 non-null object
Use 1560 non-null object
Sharing 1560 non-null object
Reporting 1560 non-null object
Student_Type 1560 non-null object
Position_Type 1560 non-null object
Object_Type 1560 non-null object
Pre_K 1560 non-null object
Operating_Status 1560 non-null object
Object_Description 1461 non-null object
Text_2 382 non-null object
SubFund_Description 1183 non-null object
Job_Title_Description 1131 non-null object
Text_3 677 non-null object
Text_4 193 non-null object
Sub_Object_Description 364 non-null object
Location_Description 874 non-null object
FTE 449 non-null float64
Function_Description 1340 non-null object
Facility_or_Department 252 non-null object
Position_Extra 1026 non-null object
Total 1542 non-null float64
Program_Description 1192 non-null object
Fund_Description 819 non-null object
Text_1 1132 non-null object
dtypes: float64(2), int64(1), object(23)
memory usage: 317.0+ KB

简单描述:

1
2
3
4
5
6
7
8
9
10
11
In [5]: df.describe()
Out[5]:
Unnamed: 0 FTE Total
count 1560.000000 449.000000 1.542000e+03
mean 227767.180128 0.493532 1.446867e+04
std 130207.535688 0.452844 7.916752e+04
min 198.000000 -0.002369 -1.044084e+06
25% 113690.750000 NaN NaN
50% 226445.500000 NaN NaN
75% 340883.500000 NaN NaN
max 450277.000000 1.047222 1.367500e+06

FTE 全职员工数

1
2
3
4
5
6
7
8
In [4]: df.FTE.head()
Out[4]:
198 NaN
209 NaN
750 1.0
931 NaN
1524 NaN
Name: FTE, dtype: float64

数据里的FTE为(Full Time equivalent)全职员工的意思,在我们的数据中,如果一项预算与一个员工有关,这个值就反应了这个员工的全职工作的百分比。

  • 1,全职员工
  • 0,兼职或者合同制员工

这个值本身是有许多的数据缺失的,所以如果要使用的话,需要先将na值去掉。

将FTE绘图,可以看到,这所学校的兼职员工和全职员工的支出很高,而中间的数据则比较少。

数据类型

我们的数据中,有些列只有特定的值,比如:

1
2
3
df.label.unique()

['a','b']

我们需要将其变成数字来处理,一是我们的模型只能计算数字,而是可以提升速度。

在pandas中,有一种 category 类型的数据,可以干这件事。

通过pandas的 astype()方法,可以将一列数据转化成 category,转化后,就可以使用 get_dummies方法来查看 dummy variables,这会让原来的每个值都用数字来替代,如:

原始数据:

origin
0 US
1 Europe
2 Asia

get_dummies()后:

origin_Asia origin_Europe origin_US
0 0 1
0 1 0
1 0 0

这个步骤又叫做 binary indicator representation.

如果你有多列数据需要转化成 category类型,可以使用 lambda 函数加dataframe的apply方法,记得设置axis=0,也就是处理方式是按列,处理完成之后,你可以使用df.dtypes.value_counts()来查看你的数据里数据类型的分布情况。

如何定义成功?

用accuracy,没办法解决垃圾邮件的问题,这个我们在机器学习那一章已经讲过,所以我们使用log loss,简单来说,accuracy是尽可能的提高正确率,而log loss则是尽可能的降低错误率。

下面是我们使用的loss function的定义:

\[logloss=-\frac{1}{N}\sum_{i=1}^{N}(y_ilogs(p_i))+(1-y_i)log(1-p_i)\]

  • y:是否分类正确,1=yes,0=no
  • p:为1的概率

复习一下,

\[\sum\]

叫做求和符号,读作segema,它的意思就是连续的加法,比如:

\[\sum_{k=1}^{n}ak=a_1+a_2+a_3+..a_n\]

k是下标,它会从k变化到n,当k=1的时候,ak就是a1,当k=2的时候,ak就是a2,最后一项就是k=n,segima的意思呢就是把这些项全都加起来。

所以这里的segema的意思就是把数据中,每一行的数据都加起来,如果你不了解求和符号,可以参考我的这篇文章

把一行的数据乘以 \[-\frac{1}{N}\],得到这一行的logloss.

log loss函数示例

假设A

  • true label=0
  • 预测1的概率是p=0.9:

带入公式计算可得:

\[log loss=(1-y)\log^{1-p}\]

\[=\log^{1-0.9}\]

\[=\log^{0.1}\]

\[=2.3\]

假设B

  • true label=1
  • 预测0的概率是0.5

则los函数表示为:

\[logloss=-\frac{1}{1}1\log^{0.5}+(1-1)\log^{1-0.5}\]

带入公式计算可得:

\[=-\log^{0.5}\]

\[=0.69\]

log loss python实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def compute_log_loss(predicted, actual, eps=1e-14):
""" Computes the logarithmic loss between predicted and
actual when these are 1D arrays.

:param predicted: The predicted probabilities as floats between 0-1
:param actual: The actual binary labels. Either 0 or 1.
:param eps (optional): log(0) is inf, so we need to offset our
predicted values slightly by eps from 0 or 1.
"""
predicted = np.clip(predicted, eps, 1 - eps)
loss = -1 * np.mean(actual * np.log(predicted)
+ (1 - actual)
* np.log(1 - predicted))

return loss

我们可以用这个函数取计算一些提供好的值的log loss值,下面是算好后的结果:

1
2
3
4
5
Log loss, correct and confident: 0.05129329438755058
Log loss, correct and not confident: 0.4307829160924542
Log loss, wrong and not confident: 1.049822124498678
Log loss, wrong and confident: 2.9957322735539904
Log loss, actual labels: 9.99200722162646e-15

可以看到,模型的预测越准确,则logloss值越低,真实值的logloss是极低的。

建立模型

我们建模往往都是一开始建立简单的模型,然后再慢慢的优化。

所以快速的建立模型,很有必要,我们从 mutil-class logistic regression 开始,简单的模型我们就只选择数字类型的了,其他的列都不要,然后将列分开建模,然后把整个数据按行拿进来预测,观察在这一列是否有出现。

但还有一个问题,在之前的课程中,我们将数据分割成train组与test组,但这个方案在这里是不行的,而且那中情况只适合单个 target 的情况。

这里我们会使用另外一个函数来分割数据,叫做 mutil_label_train_test_split

随后我们找出所有数字类型的features作为trains set, 把我们感兴趣的 lebels 也拿出来(以get_dummies的形式),就可以利用这两组数据生成train与test数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Create the new DataFrame: numeric_data_only
numeric_data_only = df[NUMERIC_COLUMNS].fillna(-1000)

# Get labels and convert to dummy variables: label_dummies
label_dummies = pd.get_dummies(df[LABELS])

# Create training and test sets
X_train, X_test, y_train, y_test = multilabel_train_test_split(numeric_data_only,label_dummies,size=0.2,seed=123)

# Print the info
print("X_train info:")
print(X_train.info())
print("\nX_test info:")
print(X_test.info())
print("\ny_train info:")
print(y_train.info())
print("\ny_test info:")
print(y_test.info())

有了traning数据之后,就可以训练模型并计算我们的模型得分了,这里为了将我们的列分开计算,我们需要使用 ‌OneVsRestClassifier 包,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Import classifiers
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier

# Create the DataFrame: numeric_data_only
numeric_data_only = df[NUMERIC_COLUMNS].fillna(-1000)

# Get labels and convert to dummy variables: label_dummies
label_dummies = pd.get_dummies(df[LABELS])

# Create training and test sets
X_train, X_test, y_train, y_test = multilabel_train_test_split(numeric_data_only,label_dummies,size=0.2, seed=123)

# Instantiate the classifier: clf
clf = OneVsRestClassifier(LogisticRegression())

# Fit the classifier to the training data
clf.fit(X_train,y_train)

# Print the accuracy
print("Accuracy: {}".format(clf.score(X_test,y_test)))

预测

这一次我们利用真实的数据来预测,这些数据是模型从未见过的,我们通过pd.read_csv导入它

1
2
3
4
5
6
7
8
9
10
11
# Instantiate the classifier: clf
clf = OneVsRestClassifier(LogisticRegression())

# Fit it to the training data
clf.fit(X_train, y_train)

# Load the holdout data: holdout
holdout = pd.read_csv("HoldoutData.csv",index_col=0)

# Generate predictions: predictions
predictions = clf.predict_proba(holdout[NUMERIC_COLUMNS].fillna(-1000))

自然语言处理简要概述(NLP)

Tokenizing 与 gram

Tokenizing即将文本变成词语,简单的直接按照空格或者是标点符号分割,复杂一点会有自此识别,如结巴分词中:

1
我来到北京清华大学

变成:

1
我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学

在上面的例子中,gram的选择会有不一样的效果,如果gram=1,则结果变成:

1
我/ 来/到/ 北/京/ 清/华/ 大/学

如果gram=2,则:

1
我来/到北/京清/华大/学

文本到数字

上面的语句变成词语组,这些词语就叫做 bag of words

在 sklearn中,有一个方法可以计算bag of words,CountVectorizer

使用之前,你需要给CountVectorizer传入一个正则表达式,它才知道如何去分割字词,对了,不要忘记处理你数据中的missing value.

当你用正则创建好CountVectorizer后,就可以把语句传进来计算bag of words,一样它也是使用fit方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Import CountVectorizer
from sklearn.feature_extraction.text import CountVectorizer

# Create the token pattern: TOKENS_ALPHANUMERIC
TOKENS_ALPHANUMERIC = '[A-Za-z0-9]+(?=\\s+)'

# Fill missing values in df.Position_Extra
df.Position_Extra.fillna('',inplace=True)

# Instantiate the CountVectorizer: vec_alphanumeric
vec_alphanumeric = CountVectorizer(token_pattern='[A-Za-z0-9]+(?=\s+)')

# Fit to the data
vec_alphanumeric.fit(df.Position_Extra)

# Print the number of tokens and first 15 tokens
msg = "There are {} tokens in Position_Extra if we split on non-alpha numeric"
print(msg.format(len(vec_alphanumeric.get_feature_names())))
print(vec_alphanumeric.get_feature_names()[:15])

改善您的模型

Pipelines, feature & text preprocessing

我们已经接触过pipline了,它可以让我们处理数据的步骤变得更容易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Import Pipeline
from sklearn.pipeline import Pipeline
# Import other necessary modules
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier

# Split and select numeric data only, no nans
X_train, X_test, y_train, y_test = train_test_split(sample_df[['numeric']],pd.get_dummies(sample_df['label']),random_state=22)

# Instantiate Pipeline object: pl
pl = Pipeline([
('clf', OneVsRestClassifier(LogisticRegression()))
])
# Fit the pipeline to the training data
pl.fit(X_train, y_train)

# Compute and print accuracyaccuracy = pl.score(X_test, y_test)
print("\nAccuracy on sample data - numeric, no nans: ", accuracy)

对于pipline,它是按照顺序来执行里面的步骤的,所以您需要规划好顺序,例如对于你的数据,如果有缺失值,你必须在训练模型之前就将这些na值处理好:

1
2
3
4
pl = Pipeline([
('imp', Imputer()),
('clf', OneVsRestClassifier(LogisticRegression()))
])

这里使用了inputer来处理缺失值。

pipline对象的使用一样也是调用fit方法,并且pipline对象还提供了打分的功能(默认是accuracy)

1
2
3
4
5
# Fit the pipeline to the training data
pl.fit(X_train,y_train)

# Compute and print accuracy
accuracy = pl.score(X_test,y_test)

Text features and feature unions

对于我们的text值,不可以把它和数值型的数据一起处理,即不可以放在同一个pipline中。

怎么办呢?解决方案是使用Function Transformer()和FeatureUnion()

Function Transformer()做的事情很简单,就是接受一个python函数,把它转化成sklearn可以理解的对象,然后用它去处理数据。

我们可以写两个函数,都接受所有的dataframe,但是一个输出text的处理结果,另一个输出数值型数据的结果。

这样我们就可以对数值型数据与text型数据分别设置两套pipline。

在 Function Transformer() 参数重,我们将参数 validate设置为false,以让其不检查空值。

FeatureUnion是另一个我们需要用到的包,当我们用function transformer的时候,一个是数值型数据,另一个是文本型数据,FeatureUnion可以把这两个features联合到一起作为同一个数组,作为classifier的输入。

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
# Import FeatureUnion
from sklearn.pipeline import FeatureUnion

# Split using ALL data in sample_df
X_train, X_test, y_train, y_test = train_test_split(sample_df[['numeric', 'with_missing', 'text']],pd.get_dummies(sample_df['label']), random_state=22)

# Create a FeatureUnion with nested pipeline: process_and_join_features
process_and_join_features = FeatureUnion(
transformer_list = [
('numeric_features', Pipeline([
('selector', get_numeric_data),
('imputer', Imputer())
])),
('text_features', Pipeline([
('selector', get_text_data),
('vectorizer', CountVectorizer())
]))
]
)

# Instantiate nested pipeline: pl
pl = Pipeline([
('union', process_and_join_features),
('clf', OneVsRestClassifier(LogisticRegression()))
])


# Fit pl to the training data
pl.fit(X_train, y_train)

# Compute and print accuracy
accuracy = pl.score(X_test, y_test)
print("\nAccuracy on sample data - all data: ", accuracy)

回归学校数据

对于之前的问题,我们的数据中只有一列是文本型数据,而学校数据中,则有14列。

我们需要将这些列都结合起来,这个函数已经写好了,叫做 combine_text_columns, 定义好之后,只需要在定义Function Transformer()的时候更改一下参数就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Import FunctionTransformer
from sklearn.preprocessing import FunctionTransformer

# Get the dummy encoding of the labels
dummy_labels = pd.get_dummies(df[LABELS])

# Get the columns that are features in the original df
NON_LABELS = [c for c in df.columns if c not in LABELS]

# Split into training and test sets
X_train, X_test, y_train, y_test = multilabel_train_test_split(df[NON_LABELS],dummy_labels,0.2, seed=123)

# Preprocess the text data: get_text_data
get_text_data = FunctionTransformer(combine_text_columns,validate=False)

# Preprocess the numeric data: get_numeric_data
get_numeric_data = FunctionTransformer(lambda x: x[NUMERIC_COLUMNS], validate=False)

定义好处理文本和数字的函数之后,我们就可以建立模型了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Complete the pipeline: pl
pl = Pipeline([
('union', FeatureUnion(
transformer_list = [
('numeric_features', Pipeline([
('selector', get_numeric_data),
('imputer', Imputer())
])),
('text_features', Pipeline([
('selector', get_text_data),
('vectorizer', CountVectorizer())
]))
]
)),
('clf', OneVsRestClassifier(LogisticRegression()))
])

# Fit to the training data
pl.fit(X_train,y_train)

# Compute and print accuracy
accuracy = pl.score(X_test, y_test)
print("\nAccuracy on budget dataset: ", accuracy)

可以看到pipline输出的分数。这个处理的步骤是不是很熟悉,这个步骤是通用的,而且如果你想要更换别的分类器,只需要将 LogisticRegression 改成别的就好了,例如在这个例子里,你把模型改成RandomForestClassifier,将会有0.2的提升,如果把RandomForestClassifier的参数n_estimators改成15,accuracy 还有有所增加。

专家指点

  • tokenize text,不仅仅只是根据空格与标点来处理文字
  • n-gram statistics,文字的选择我们定义gram,我们也可以同时定义多个gram,如,CountVectorizer(token_pattern=TOKENS_ALPHANUMERIC,ngram_range=(1,2)))
  • 使用预知的alpha-numeric sequences,对于text,只接受字母数字序列

统计技巧,interaction terms

来看一组例子,这两组数据中,English teacher和2nd grade 都有出现

  • English teacher for 2nd grade
  • 2nd grade - budget for English teacher

\[\beta_1x_1+\beta_2x_2+\beta_3(x_1x_2)\]

x_1 x_2 x3
0 1 x1*x2=0*1=0
1 1 x1*x2=1*1=1
  • x1,x2代表某个特定的token有出现
  • beta符号则代表其重要程度或者说相关系数
  • x3是x1、x2两者的乘积

当然,sklearn提供了一种非常直接的方式使用interaction terms,即:PolynomialFeatures,

另外还有一个叫做SparseInteractions的包,也可以做同样的事情。

最后,我们不可能因为选一个不一样的模型就改善所有的情况,模型的优化是一个渐进的步骤,可能是你将模型的某个参数调整一下,可能是你在语义处理方面更换了一个更好的工具,这些东西都需要你去根据你的实际情况作出调整。

这篇文章就到这里结束了,下一篇将会是非监督学习。