《Python数据分析与机器学习实战-唐宇迪》读书笔记第10章-特征工程
第10章特征工程
特征工程是整个机器学习中非常重要的一部分,如何对数据进行特征提取对最终结果的影响非常大。在建模过程中,一般会优先考虑算法和参数,但是数据特征才决定了整体结果的上限,而算法和参数只决定了如何逼近这个上限。特征工程其实就是要从原始数据中找到最有价值的信息,并转换成计算机所能读懂的形式。本章结合数值数据与文本数据来分别阐述如何进行数值特征与文本特征的提取。
10.1数值特征
实际数据中,最常见的就是数值特征,本节介绍几种常用的数值特征提取方法与函数。首先还是读取一份数据集,并取其中的部分特征来做实验,不用考虑数据特征的具体含义,只进行特征操作即可。
10.1.1字符串编码
1 import pandas as pd 2 import numpy as np 3 4 vg_df = pd.read_csv('datasets/vgsales.csv', encoding = "ISO-8859-1") 5 vg_df[['Name', 'Platform', 'Year', 'Genre', 'Publisher']].iloc[1:7]
上述代码生成的数据中很多特征指标都是字符串,首先假设Genre列是最终的分类结果标签,但是计算机可不认识这些字符串,此时就需要将字符转换成数值。
1 genres = np.unique(vg_df['Genre']) 2 genres
array(['Action', 'Adventure', 'Fighting', 'Misc', 'Platform', 'Puzzle', 'Racing', 'Role-Playing', 'Shooter', 'Simulation', 'Sports', 'Strategy'], dtype=object)
读入数据后,最常见的情况就是很多特征并不是数值类型,而是用字符串来描述的,打印结果后发现,Genre列一共有12个不同的属性值,将其转换成数值即可,最简单的方法就是用数字进行映射:
1 from sklearn.preprocessing import LabelEncoder 2 3 gle = LabelEncoder() 4 genre_labels = gle.fit_transform(vg_df['Genre']) 5 genre_mappings = {index: label for index, label in enumerate(gle.classes_)} 6 genre_mappings
{0: 'Action', 1: 'Adventure', 2: 'Fighting', 3: 'Misc', 4: 'Platform', 5: 'Puzzle', 6: 'Racing', 7: 'Role-Playing', 8: 'Shooter', 9: 'Simulation', 10: 'Sports', 11: 'Strategy'}
使用sklearn工具包中的LabelEncoder()函数可以快速地完成映射工作,默认是从数值0开始,fit_transform()是实际执行的操作,自动对属性特征进行映射操作。变换完成之后,可以将新得到的结果加入原始DataFrame中对比一下:
1 vg_df['GenreLabel'] = genre_labels 2 vg_df[['Name', 'Platform', 'Year', 'Genre', 'GenreLabel']].iloc[1:7]
此时所有的字符型特征就转换成相应的数值,也可以自定义一份映射。
1 poke_df = pd.read_csv('datasets/Pokemon.csv', encoding='utf-8') 2 poke_df.head()
1 poke_df = poke_df.sample(random_state=1, frac=1).reset_index(drop=True) 2 3 np.unique(poke_df['Generation'])
array(['Gen 1', 'Gen 2', 'Gen 3', 'Gen 4', 'Gen 5', 'Gen 6'], dtype=object)
这份数据集中同样有多个属性值需要映射,也可以自己动手写一个map函数,对应数值就从1开始吧:
1 gen_ord_map = {'Gen 1': 1, 'Gen 2': 2, 'Gen 3': 3, 2 'Gen 4': 4, 'Gen 5': 5, 'Gen 6': 6} 3 4 poke_df['GenerationLabel'] = poke_df['Generation'].map(gen_ord_map) 5 poke_df[['Name', 'Generation', 'GenerationLabel']].iloc[4:10]
对于简单的映射操作,无论自己完成还是使用工具包中现成的命令都非常容易,但是更多的时候,对这种属性特征可以选择独热编码,虽然操作稍微复杂些,但从结果上观察更清晰:
1 from sklearn.preprocessing import OneHotEncoder, LabelEncoder 2 3 # 完成LabelEncoder 4 gen_le = LabelEncoder() 5 gen_labels = gen_le.fit_transform(poke_df['Generation']) 6 poke_df['Gen_Label'] = gen_labels 7 8 poke_df_sub = poke_df[['Name', 'Generation', 'Gen_Label', 'Legendary']] 9 10 # 完成OneHotEncoder 11 gen_ohe = OneHotEncoder() 12 gen_feature_arr = gen_ohe.fit_transform(poke_df[['Gen_Label']]).toarray() 13 gen_feature_labels = list(gen_le.classes_) 14 15 # 将转换好的特征组合到dataframe中 16 gen_features = pd.DataFrame(gen_feature_arr, columns=gen_feature_labels) 17 poke_df_ohe = pd.concat
18 poke_df_ohe.head()
上述代码首先导入了OneHotEncoder工具包,对数据进行数值映射操作,又进行独热编码。输出结果显示,独热编码相当于先把所有可能情况进行展开,然后分别用0和1表示实际特征情况,0代表不是当前列特征,1代表是当前列特征。例如,当Gen_Label=3时,对应的独热编码就是,Gen4为1,其余位置都为0(注意原索引从0开始,Gen_Label=3,相当于第4个位置)。
上述代码看起来有点麻烦,那么有没有更简单的方法呢?其实直接使用Pandas工具包更方便:
1 gen_dummy_features = pd.get_dummies(poke_df['Generation'], drop_first=True) 2 pd.concat([poke_df[['Name', 'Generation']], gen_dummy_features], axis=1).iloc[4:10]
Get_dummies()函数可以完成独热编码的工作,当特征较多时,一个个命名太麻烦,此时可以直接指定一个前缀用于标识:
1 gen_onehot_features = pd.get_dummies(poke_df['Generation'],prefix = 'one-hot') 2 pd.concat([poke_df[['Name', 'Generation']], gen_onehot_features], axis=1).iloc[4:10]
现在所有执行独热编码的特征全部带上“one-hot”前缀了,对比发现还是get_dummies()函数更好用,1行代码就能解决问题。
1 poke_df = pd.read_csv('datasets/Pokemon.csv', encoding='utf-8')
2 poke_df.head()
10.1.2二值与多项式特征
接下来打开一份音乐数据集:
1 popsong_df = pd.read_csv('datasets/song_views.csv', encoding='utf-8') 2 popsong_df.head(10)
数据中包括不同用户对歌曲的播放量,可以发现很多歌曲的播放量都是0,表示该用户还没有播放过此音乐,这个时候可以设置一个二值特征,以表示用户是否听过该歌曲:
1 watched = np.array(popsong_df['listen_count']) 2 watched[watched >= 1] = 1 3 popsong_df['watched'] = watched 4 popsong_df.head(10)
新加入的watched特征表示歌曲是否被播放,同样也可以使用sklearn工具包中的Binarizer来完成二值特征:
1 from sklearn.preprocessing import Binarizer 2 3 bn = Binarizer(threshold=0.9) 4 pd_watched = bn.transform([popsong_df['listen_count']])[0] 5 popsong_df['pd_watched'] = pd_watched 6 popsong_df.head(10)
特征的变换方法还有很多,还可以对其进行各种组合。接下来登场的就是多项式特征,例如有a、b两个特征,那么它的2次多项式为(1,a,b,a2,ab,b2),下面通过sklearn工具包完成变换操作:
1 poke_df = pd.read_csv('datasets/Pokemon.csv', encoding='utf-8') 2 atk_def = poke_df[['Attack', 'Defense']] 3 atk_def.head() 4 5 from sklearn.preprocessing import PolynomialFeatures 6 7 pf = PolynomialFeatures(degree=2, interaction_only=False, include_bias=False) 8 res = pf.fit_transform(atk_def) 9 res[:5]
Attack Defense
0 49 49
1 62 63
2 82 83
3 100 123
4 52 43
array([[ 49., 49., 2401., 2401., 2401.], [ 62., 63., 3844., 3906., 3969.], [ 82., 83., 6724., 6806., 6889.], [ 100., 123., 10000., 12300., 15129.], [ 52., 43., 2704., 2236., 1849.]])
PolynomialFeatures()函数涉及以下3个参数。
- degree:控制多项式的度,如果设置的数值越大,特征结果也会越多。
- interaction_only:默认为False。如果指定为True,那么不会有特征自己和自己结合的项,例如上面的二次项中没有a2和b2。
- include_bias:默认为True。如果为True的话,那么会新增1列。
为了更清晰地展示,可以加上操作的列名:
1 intr_features = pd.DataFrame(res, columns=['Attack', 'Defense', 'Attack^2', 'Attack x Defense', 'Defense^2']) 2 intr_features.head(5)
10.1.3连续值离散化
连续值离散化的操作非常实用,很多时候都需要对连续值特征进行这样的处理,效果如何还得实际通过测试集来观察,但在特征工程构造的初始阶段,肯定还是希望可行的路线越多越好。
1 cc_survey_df = pd.read_csv('datasets/fcc_2016_coder_survey_subset.csv', encoding='utf-8') 2 fcc_survey_df[['ID.x', 'EmploymentField', 'Age', 'Income']].head()
上述代码读取了一份带有年龄信息的数据集,接下来要对年龄特征进行离散化操作,也就是划分成一个个区间,实际操作之前,可以观察其分布情况:
1 import pandas as pd 2 import matplotlib.pyplot as plt 3 import matplotlib as mpl 4 import numpy as np 5 import scipy.stats as spstats 6 7 %matplotlib inline 8 mpl.style.reload_library() 9 mpl.style.use('classic') 10 mpl.rcParams['figure.facecolor'] = (1, 1, 1, 0) 11 mpl.rcParams['figure.figsize'] = [6.0, 4.0] 12 mpl.rcParams['figure.dpi'] = 100 13 14 fig, ax = plt.subplots() 15 fcc_survey_df['Age'].hist(color='#A9C5D3') 16 ax.set_title('Developer Age Histogram', fontsize=12) 17 ax.set_xlabel('Age', fontsize=12) 18 ax.set_ylabel('Frequency', fontsize=12)
上述输出结果显示,年龄特征的取值范围在10~90之间。所谓离散化,就是将一段区间上的数据映射到一个组中,例如按照年龄大小可分成儿童、青年、中年、老年等。简单起见,这里直接按照相同间隔进行划分:
1 fcc_survey_df['Age_bin_round'] = np.array(np.floor(np.array(fcc_survey_df['Age']) / 10.)) 2 fcc_survey_df[['ID.x', 'Age', 'Age_bin_round']].iloc[1071:1076]
上述代码中,np.floor表示向下取整,例如,对3.3取整后,得到的就是3。这样就完成了连续值的离散化,所有数值都划分到对应的区间上。
还可以利用分位数进行分箱操作,换一个特征试试,先来看看收入的情况:
1 #fcc_survey_df[['ID.x', 'Age', 'Income']].iloc[4:9] 2 fig, ax = plt.subplots() 3 fcc_survey_df['Income'].hist(bins=30, color='#A9C5D3') 4 ax.set_title('Developer Income Histogram', fontsize=12) 5 ax.set_xlabel('Developer Income', fontsize=12) 6 ax.set_ylabel('Frequency', fontsize=12)
分位数就是按照比例来划分,也可以自定义合适的比例:
1 quantile_list = [0, .25, .5, .75, 1.] 2 quantiles = fcc_survey_df['Income'].quantile(quantile_list) 3 quantiles
0.00 6000.0
0.25 20000.0
0.50 37000.0
0.75 60000.0
1.00 200000.0
Name: Income, dtype: float64
1 fig, ax = plt.subplots() 2 fcc_survey_df['Income'].hist(bins=30, color='#A9C5D3') 3 4 for quantile in quantiles: 5 qvl = plt.axvline(quantile, color='r') 6 ax.legend([qvl], ['Quantiles'], fontsize=10) 7 8 ax.set_title('Developer Income Histogram with Quantiles', fontsize=12) 9 ax.set_xlabel('Developer Income', fontsize=12) 10 ax.set_ylabel('Frequency', fontsize=12)
Quantile函数就是按照选择的比例得到对应的切分值,再应用到数据中进行离散化操作即可:
1 quantile_labels = ['0-25Q', '25-50Q', '50-75Q', '75-100Q'] 2 fcc_survey_df['Income_quantile_range'] = pd.qcut(fcc_survey_df['Income'], 3 q=quantile_list) 4 fcc_survey_df['Income_quantile_label'] = pd.qcut(fcc_survey_df['Income'], 5 q=quantile_list, labels=quantile_labels) 6 fcc_survey_df[['ID.x', 'Age', 'Income', 7 'Income_quantile_range', 'Income_quantile_label']].iloc[4:9]
此时所有数据都完成了分箱操作,拿到实际数据后如何指定比例就得看具体问题,并没有固定不变的规则,根据实际业务来判断才是最科学的。
10.1.4对数与时间变换
拿到某列数据特征后,其分布可能是各种各样的情况,但是,很多机器学习算法希望预测的结果值能够呈现高斯分布,这就需要再对其进行变换,最直接的就是对数变换:
1 fcc_survey_df['Income_log'] = np.log((1+ fcc_survey_df['Income'])) 2 fcc_survey_df[['ID.x', 'Age', 'Income', 'Income_log']].iloc[4:9]
1 income_log_mean = np.round(np.mean(fcc_survey_df['Income_log']), 2) 2 3 fig, ax = plt.subplots() 4 fcc_survey_df['Income_log'].hist(bins=30, color='#A9C5D3') 5 plt.axvline(income_log_mean, color='r') 6 ax.set_title('Developer Income Histogram after Log Transform', fontsize=12) 7 ax.set_xlabel('Developer Income (log scale)', fontsize=12) 8 ax.set_ylabel('Frequency', fontsize=12) 9 ax.text(11.5, 450, r'$\mu$='+str(income_log_mean), fontsize=10)
经过对数变换之后,特征分布更接近高斯分布,虽然还不够完美,但还是有些进步的,感兴趣的读者还可以进一步了解cox-box变换,目的都是相同的,只是在公式上有点区别。
时间相关数据也是可以提取出很多特征,例如年、月、日、小时等,甚至上旬、中旬、下旬、工作时间、下班时间等都可以当作算法的输入特征。
1 import datetime 2 import numpy as np 3 import pandas as pd 4 from dateutil.parser import parse 5 import pytz 6 7 import numpy as np 8 import pandas as pd 9 10 time_stamps = ['2015-03-08 10:30:00.360000+00:00', '2017-07-13 15:45:05.755000-07:00', 11 '2012-01-20 22:30:00.254000+05:30', '2016-12-25 00:30:00.000000+10:00'] 12 df = pd.DataFrame(time_stamps, columns=['Time']) 13 df
Time
0 2015-03-08 10:30:00.360000+00:00
1 2017-07-13 15:45:05.755000-07:00
2 2012-01-20 22:30:00.254000+05:30
3 2016-12-25 00:30:00.000000+10:00
接下来就要得到各种细致的时间特征,如果用的是标准格式的数据,也可以直接调用其属性,更方便一些:
ts_objs = np.array([pd.Timestamp(item) for item in np.array(df.Time)]) df['TS_obj'] = ts_objs ts_objs
array([Timestamp('2015-03-08 10:30:00.360000+0000', tz='UTC'), Timestamp('2017-07-13 15:45:05.755000-0700', tz='pytz.FixedOffset(-420)'), Timestamp('2012-01-20 22:30:00.254000+0530', tz='pytz.FixedOffset(330)'), Timestamp('2016-12-25 00:30:00+1000', tz='pytz.FixedOffset(600)')], dtype=object)
1 df['Year'] = df['TS_obj'].apply(lambda d: d.year) 2 df['Month'] = df['TS_obj'].apply(lambda d: d.month) 3 df['Day'] = df['TS_obj'].apply(lambda d: d.day) 4 df['DayOfWeek'] = df['TS_obj'].apply(lambda d: d.dayofweek) 5 # df['DayName'] = df['TS_obj'].apply(lambda d: d.weekday_name)# 6 # AttributeError: 'Timestamp' object has no attribute 'weekday_name' 7 df['DayOfYear'] = df['TS_obj'].apply(lambda d: d.dayofyear) 8 df['WeekOfYear'] = df['TS_obj'].apply(lambda d: d.weekofyear) 9 df['Quarter'] = df['TS_obj'].apply(lambda d: d.quarter) 10 11 # df[['Time', 'Year', 'Month', 'Day', 'Quarter', 12 # 'DayOfWeek', 'DayName', 'DayOfYear', 'WeekOfYear']] 13 df[['Time', 'Year', 'Month', 'Day', 'Quarter', 14 'DayOfWeek', 'DayOfYear', 'WeekOfYear']]
1 hour_bins = [-1, 5, 11, 16, 21, 23] 2 bin_names = ['Late Night', 'Morning', 'Afternoon', 'Evening', 'Night'] 3 df['TimeOfDayBin'] = pd.cut(df['Hour'], 4 bins=hour_bins, labels=bin_names) 5 df[['Time', 'Hour', 'TimeOfDayBin']]
Time Hour TimeOfDayBin 0 2015-03-08 10:30:00.360000+00:00 10 Morning 1 2017-07-13 15:45:05.755000-07:00 15 Afternoon 2 2012-01-20 22:30:00.254000+05:30 22 Night 3 2016-12-25 00:30:00.000000+10:00 0 Late Night
原始时间特征确定后,竟然分出这么多小特征。当拿到具体时间数据后,还可以整合一些相关信息,例如天气情况,气象台数据很轻松就可以拿到,对应的温度、降雨等指标也就都有了。
10.2文本特征
文本特征经常在数据中出现,一句话、一篇文章都是文本特征。还是同样的问题,计算机依旧不认识它们,所以首先要将其转换成数值,也就是向量。关于文本特征的提取方式,这里先做简单介绍,在下一章的新闻分类任务中,还会详细解释文本特征提取操作。
10.2.1词袋模型
先来构造一个数据集,简单起见就用英文表示,如果是中文数据,还需要先进行分词操作,英文中默认就是分好词的结果:
1 import pandas as pd 2 import numpy as np 3 import re 4 import nltk #pip install nltk 5 #jieba 6 7 corpus = ['The sky is blue and beautiful.', 8 'Love this blue and beautiful sky!', 9 'The quick brown fox jumps over the lazy dog.', 10 'The brown fox is quick and the blue dog is lazy!', 11 'The sky is very blue and the sky is very beautiful today', 12 'The dog is lazy but the brown fox is quick!' 13 ] 14 labels = ['weather', 'weather', 'animals', 'animals', 'weather', 'animals'] 15 corpus = np.array(corpus) 16 corpus_df = pd.DataFrame({'Document': corpus, 17 'Category': labels}) 18 corpus_df = corpus_df[['Document', 'Category']] 19 corpus_df
Document Category 0 The sky is blue and beautiful. weather 1 Love this blue and beautiful sky! weather 2 The quick brown fox jumps over the lazy dog. animals 3 The brown fox is quick and the blue dog is lazy! animals 4 The sky is very blue and the sky is very beaut... weather 5 The dog is lazy but the brown fox is quick! animals
在自然语言处理中有一个非常实用的NLTK工具包,使用前需要先安装该工具包,但是,安装完之后,它相当于一个空架子,里面没有实际的功能,需要有选择地安装部分插件(见图10-1)。
图10-1 NLTK工具包
执行nltk.download()会跳出安装界面,选择需要的功能进行安装即可。不仅如此,NLTK工具包还提供了很多数据集供我们练习使用,功能还是非常强大的。
NLTK安装可以参考这里:《数据分析实战-托马兹.卓巴斯》读书笔记第9章--自然语言处理NLTK(分析文本、词性标注、主题抽取、文本数据分类)
1 nltk.download() 2 # nltk.download('wordnet') 3 #并把文件从默认的路径C:\Users\tony zhang\AppData\Roaming\nltk_data\移动到D:\download\nltk_data\
1 from nltk import data 2 data.path.append(r'D:\download\nltk_data') # 这里的路径需要换成自己数据文件下载的路径
对于文本数据,第一步肯定要进行预处理操作,基本的套路就是去掉各种特殊字符,还有一些用处不大的停用词。
所谓停用词就是该词对最终结果影响不大,例如,“我们”“今天”“但是”等词语就属于停用词。
1 import nltk 2 from nltk import data 3 data.path.append(r'D:\download\nltk_data') # 这里的路径需要换成自己数据文件下载的路径 4 #加载停用词 5 wpt = nltk.WordPunctTokenizer() 6 stop_words = nltk.corpus.stopwords.words('english') 7 8 def normalize_document(doc): 9 # 去掉特殊字符 10 doc = re.sub(r'[^a-zA-Z0-9\s]', '', doc, re.I) 11 # 转换成小写 12 doc = doc.lower() 13 doc = doc.strip() 14 # 分词 15 tokens = wpt.tokenize(doc) 16 # 去停用词 17 filtered_tokens = [token for token in tokens if token not in stop_words] 18 # 重新组合成文章 19 doc = ' '.join(filtered_tokens) 20 return doc 21 22 normalize_corpus = np.vectorize(normalize_document)
1 norm_corpus = normalize_corpus(corpus) 2 norm_corpus 3 #The sky is blue and beautiful.
array(['sky blue beautiful', 'love blue beautiful sky', 'quick brown fox jumps lazy dog', 'brown fox quick blue dog lazy', 'sky blue sky beautiful today', 'dog lazy brown fox quick'], dtype='<U30')
像the、this等对整句话的主题不起作用的词也全部去掉,下面就要对文本进行特征提取,也就是把每句话都转换成数值向量。
1 from sklearn.feature_extraction.text import CountVectorizer 2 print (norm_corpus) 3 cv = CountVectorizer(min_df=0., max_df=1.) 4 cv.fit(norm_corpus) 5 print (cv.get_feature_names()) 6 cv_matrix = cv.fit_transform(norm_corpus) 7 cv_matrix = cv_matrix.toarray() 8 cv_matrix
['sky blue beautiful' 'love blue beautiful sky' 'quick brown fox jumps lazy dog' 'brown fox quick blue dog lazy' 'sky blue sky beautiful today' 'dog lazy brown fox quick'] ['beautiful', 'blue', 'brown', 'dog', 'fox', 'jumps', 'lazy', 'love', 'quick', 'sky', 'today'] array([[1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0], [1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0], [0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0], [0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0], [1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 1], [0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0]], dtype=int64)
1 vocab = cv.get_feature_names()
2 pd.DataFrame(cv_matrix, columns=vocab)
文章中出现多少个不同的词,其向量的维度就是多大,再依照其出现的次数和位置,就可以把向量构造出来。上述代码只考虑单个词,其实还可以把词和词之间的组合考虑进来,原理还是一样的,接下来就要多考虑组合,从结果来看更直接:
1 bv = CountVectorizer(ngram_range=(2,2)) 2 bv_matrix = bv.fit_transform(norm_corpus) 3 bv_matrix = bv_matrix.toarray() 4 vocab = bv.get_feature_names() 5 pd.DataFrame(bv_matrix, columns=vocab)
上述代码设置了ngram_range参数,相当于要考虑词的上下文,此处只考虑两两组合的情况,大家也可以将ngram_range参数设置成(1,2),这样既包括一个词也包括两个词组合的情况。
词袋模型的原理和操作都十分简单,但是这样做出来的向量是没有灵魂的。无论是一句话还是一篇文章,都是有先后顺序的,但在词袋模型中,却只考虑词频,并且每个词的重要程度完全和其出现的次数相关,通常情况下,文章向量会是一个非常大的稀疏矩阵,并不利于计算。
词袋模型的问题看起来还是很多,其优点也是有的,简单方便。在实际建模任务中,还不能确定哪种特征提取方法效果更好,所以,各种方法都需要尝试。
10.2.2常用文本特征构造方法
文本特征提取方法还很多,下面介绍一些常用的构造方法,在实际任务中,不仅可以选择常规套路,也可以组合使用一些野路子。
(1)TF-IDF特征。虽然词袋模型只考虑了词频,没考虑词本身的含义,但在TF-IDF中,会考虑每个词的重要程度,后续再详细讲解TF-IDF关键词的提取方法,先来看看其能得到的结果:
1 from sklearn.feature_extraction.text import TfidfVectorizer 2 tv = TfidfVectorizer(min_df=0., max_df=1., use_idf=True) 3 tv_matrix = tv.fit_transform(norm_corpus) 4 tv_matrix = tv_matrix.toarray() 5 6 vocab = tv.get_feature_names() 7 pd.DataFrame(np.round(tv_matrix, 2), columns=vocab)
上述输出结果显示,每个词都得到一个小数结果,并且有大小之分,表明其在该篇文章中的重要程度,下一章的新闻分类任务还会详细讨论。
(2)相似度特征。只要确定了特征,并且全部转换成数值数据,才可以计算它们之间的相似性,计算方法也比较多,这里用余弦相似性来举例,sklearn工具包中已经有实现好的功能,直接将上例中TF-IDF特征提取结果当作输入即可:
1 from sklearn.metrics.pairwise import cosine_similarity 2 3 similarity_matrix = cosine_similarity(tv_matrix) 4 similarity_df = pd.DataFrame(similarity_matrix) 5 similarity_df
(3)聚类特征。聚类就是把数据按堆划分,最后每堆给出一个实际的标签,需要先把数据转换成数值特征,然后计算其聚类结果,其结果也可以当作离散型特征(聚类算法会在第16章讲解)。
1 from sklearn.cluster import KMeans 2 3 km = KMeans(n_clusters=2) 4 km.fit_transform(similarity_df) 5 cluster_labels = km.labels_ 6 cluster_labels = pd.DataFrame(cluster_labels, columns=['ClusterLabel']) 7 pd.concat([corpus_df, cluster_labels], axis=1)
(4)主题模型。主题模型是无监督方法,输入就是处理好的语料库,可以得到主题类型以及其中每一个词的权重结果:
1 from sklearn.decomposition import LatentDirichletAllocation 2 3 # help(LatentDirichletAllocation) 4 # lda = LatentDirichletAllocation(n_topics=2, max_iter=100, random_state=42) 5 # n_components : int, optional (default=10) 6 # | Number of topics. 7 8 lda = LatentDirichletAllocation(n_components=2, max_iter=100, random_state=42) 9 dt_matrix = lda.fit_transform(tv_matrix) 10 features = pd.DataFrame(dt_matrix, columns=['T1', 'T2']) 11 features 12 13 tt_matrix = lda.components_ 14 for topic_weights in tt_matrix: 15 topic = [(token, weight) for token, weight in zip(vocab, topic_weights)] 16 topic = sorted(topic, key=lambda x: -x[1]) 17 topic = [item for item in topic if item[1] > 0.6] 18 print(topic) 19 print()
T1 T2
0 0.190548 0.809452
1 0.176804 0.823196
2 0.846184 0.153816
3 0.814863 0.185137
4 0.180516 0.819484
5 0.839172 0.160828
[('brown', 1.7273638692668465), ('dog', 1.7273638692668465), ('fox', 1.7273638692668465), ('lazy', 1.7273638692668465), ('quick', 1.7273638692668465),
('jumps', 1.0328325272484777), ('blue', 0.7731573162915626)] [('sky', 2.264386643135622), ('beautiful', 1.9068269319456903), ('blue', 1.7996282104933266), ('love', 1.148127242397004),
('today', 1.0068251160429935)]
上述代码设置n_topicsn_components =2,相当于要得到两种主题,最后的结果就是各个主题不同关键词的权重,看起来这件事处理得还不错,使用无监督的方法,也能得到这么多关键的指标。笔者认为,LDA主题模型并不是很实用,得到的效果通常也是一般,所以,并不建议大家用其进行特征处理或者建模任务,熟悉一下就好。
(5)词向量模型。前面介绍的几种特征提取方法还是比较容易理解的,再来看看词向量模型,也就是常说的word2vec,其基本原理是基于神经网络的。先来通俗地解释一下,首先对每个词进行初始化操作,例如,每个词都是长度为10的一个随机向量。接下来,模型会对每个词及其上下文进行预测,例如输入是向量“回家”,输出就是“吃饭”,所有的输入数据和输出标签都是语料库中的上下文,所以标签并不需要特意指定。此时不只要通过优化算法选择合适的权重参数,例如梯度下降,输入的向量也会随之改变,也就是向量“回家”一开始是随机的,在每次迭代过程中都会不断改变,直到得到一个合适的结果。
词向量模型是现阶段自然语言处理中最常使用的方法,并赋予每个词实际的空间含义,回顾一下,使用前面讲述过的特征提取方法得到的向量都没有实际意义,只是数值,但在词向量模型中,每个词在空间中都是有实际意义的,例如,“喜欢”和“爱”这两个词在空间中比较接近,因为其表达的含义类似,但是它们和“手机”就离得比较远,因为关系不大。讲解完神经网络之后,在第20章的影评分类任务中有它的实际应用案例。当大家使用时,需首先将文本中每一个词的向量构造出来,最常用的工具包就是Gensim,其中有语料库:
1 from gensim.models import word2vec 2 from nltk import data 3 data.path.append(r'D:\download\nltk_data') # 这里的路径需要换成自己数据文件下载的路径 4 wpt = nltk.WordPunctTokenizer() 5 tokenized_corpus = [wpt.tokenize(document) for document in norm_corpus] 6 7 # 需要设置一些参数 8 feature_size = 10 # 词向量维度 9 window_context = 10 # 滑动窗口 10 min_word_count = 1 # 最小词频 11 12 w2v_model = word2vec.Word2Vec(tokenized_corpus, size=feature_size, 13 window=window_context, min_count = min_word_count) 14 15 w2v_model.wv['sky']
array([-0.02571594, -0.02806569, -0.01904523, -0.03620922, 0.01884929, -0.04410132, 0.02005241, -0.00504071, 0.01696092, 0.01301065], dtype=float32)
1 def average_word_vectors(words, model, vocabulary, num_features): 2 3 feature_vector = np.zeros((num_features,),dtype="float64") 4 nwords = 0. 5 6 for word in words: 7 if word in vocabulary: 8 nwords = nwords + 1. 9 feature_vector = np.add(feature_vector, model[word]) 10 11 if nwords: 12 feature_vector = np.divide(feature_vector, nwords) 13 14 return feature_vector 15 16 17 def averaged_word_vectorizer(corpus, model, num_features): 18 vocabulary = set(model.wv.index2word) 19 features = [average_word_vectors(tokenized_sentence, model, vocabulary, num_features) 20 for tokenized_sentence in corpus] 21 return np.array(features)
1 w2v_feature_array = averaged_word_vectorizer(corpus=tokenized_corpus, model=w2v_model, 2 num_features=feature_size) 3 pd.DataFrame(w2v_feature_array) #lstm
输出结果就是输入预料中的每一个词都转换成向量,词向量的应用十分广泛,现阶段通常都是将其和神经网络结合在一起来搭配使用(后续案例就会看到其强大的战斗力)。
10.3论文与benchmark
在数据挖掘任务中,特征工程尤为重要,数据的字段中可能包含各种各样的信息,如何提取出最有价值的特征呢?大家第一个想到的可能是经验方法,回顾一下之前处理其他数据的方法或者一些通用的套路,但肯定都不确定方法是否得当,而且要把每个想法都实践一遍也不太现实。这里给大家推荐一个套路,结合论文与benchmark来找解决方案,相信会事半功倍。
最好的方法就是从论文入手,大家也可以把论文当作是一个实际任务的解决方案,对于较复杂的任务,你可能没有深入研究过,但是前人已经探索过其中的方法,论文就是他们对好的思路、实验结果以及其中遇到各种问题的总结。如果把他们的方法加以研究和改进,再应用到实际任务中,是不是看起来很棒?
但是,如何找到合适的论文作为参考呢?如果不是专门做某一领域,可能对这些资源并不是很熟悉,这里给大家推荐benchmark,翻译过来叫作“基准”。其实它就是一个数据库,里面有某一领域的数据集,并且收录很多该领域的论文,还有测试结果。
图10-2所示为迪哥曾经做过实验的benchmark,首页就是它的整体介绍。例如,对于一个人体关键点的图像识别任务,其中不仅提供了一份人体姿态的数据集,还收录很多篇相关论文,通常能被benchmark收录进来的论文都是被证明过效果非常不错的。
图10-2 MPII人体姿态识别benchmark
图10-3中截取了其收录的一部分论文,从2013—2018年的姿态识别经典论文都可以在此找到。如果大家熟悉计算机视觉领域,就能看出这些论文的发表级别非常高,右侧有其实验结果,包括头部、肩膀、各个关节的识别效果。可以发现,随着年份的增加,效果逐步提升,现在做得已经很成熟了。
图10-3 收录论文结果
对于不会选择合适论文的同学,还是看经典论文吧,直接搜索出来的论文可能价值一般,benchmark推荐的论文都是经典且有学习价值的。
Benchmark还有一个特点,就是其收录的论文很多都是有公开代码的。图10-4、图10-5就是打开的论文主页,不仅有实验的源码,还提供了训练好的模型,无论是实际完成任务还是学习阶段,都对大家有很大的帮助。假设你需要做一个人体姿态识别的任务,这时候你不只手里有一份当下效果最好的识别代码,还有原作者训练好的模型,直接部署到服务器,不出一天你就可以说:任务基本完成了,目前来看没有比这个效果更好的了(这为我们的工作提供了一条捷径)。
▲图10-4 论文公开源码(1)
▲图10-5 论文公开源码(2)
在初学阶段最好将理论与实践结合在一起,论文当然就是指导思想,告诉大家一步步该怎么做,其提供的代码就是实践方法。笔者认为没有源码的学习是非常痛苦的,因为论文当中很多细节都简化了,估计很多同学也是这样的想法,看代码反而能更直接地理解论文的思想。
如何应用源码呢?通常拿到的工作都是比较复杂的,直接看一行行代码,估计都挺费劲,最好的办法就是一步步debug,看看其中每一步完成了什么,再结合论文就好理解了。
10.4图像特征
1 pip install skimage
1 import skimage 2 import numpy as np 3 import pandas as pd 4 import matplotlib.pyplot as plt 5 from skimage import io 6 #opencv tensorflow 7 %matplotlib inline 8 9 cat = io.imread('./datasets/cat.png') 10 dog = io.imread('./datasets/dog.png') 11 df = pd.DataFrame(['Cat', 'Dog'], columns=['Image']) 12 13 14 print(cat.shape, dog.shape)
(168, 300, 3) (168, 300, 3)
1 cat #0-255,越小的值代表越暗,越大的值越亮
array([[[114, 105, 90], [113, 104, 89], [112, 103, 88], ..., [127, 130, 121], [130, 133, 124], [133, 136, 127]], [[113, 104, 89], [112, 103, 88], [111, 102, 87], ..., [129, 132, 125], [132, 135, 128], [135, 138, 131]], [[111, 102, 87], [111, 102, 87], [110, 101, 86], ..., [132, 134, 133], [136, 138, 137], [139, 141, 140]], ..., [[ 32, 26, 28], [ 32, 26, 28], [ 30, 24, 26], ..., [131, 131, 131], [131, 131, 131], [130, 130, 130]], [[ 33, 27, 29], [ 32, 26, 28], [ 31, 25, 27], ..., [131, 131, 131], [131, 131, 131], [130, 130, 130]], [[ 33, 27, 29], [ 32, 26, 28], [ 31, 25, 27], ..., [131, 131, 131], [131, 131, 131], [130, 130, 130]]], dtype=uint8)
1 #coffee = skimage.transform.resize(coffee, (300, 451), mode='reflect') 2 fig = plt.figure(figsize = (8,4)) 3 ax1 = fig.add_subplot(1,2, 1) 4 ax1.imshow(cat) 5 ax2 = fig.add_subplot(1,2, 2) 6 ax2.imshow(dog)
<matplotlib.image.AxesImage at 0x233c9b53988>
1 dog_r = dog.copy() # Red Channel 2 dog_r[:,:,1] = dog_r[:,:,2] = 0 # set G,B pixels = 0 3 dog_g = dog.copy() # Green Channel 4 dog_g[:,:,0] = dog_r[:,:,2] = 0 # set R,B pixels = 0 5 dog_b = dog.copy() # Blue Channel 6 dog_b[:,:,0] = dog_b[:,:,1] = 0 # set R,G pixels = 0 7 8 plot_image = np.concatenate((dog_r, dog_g, dog_b), axis=1) 9 plt.figure(figsize = (10,4)) 10 plt.imshow(plot_image)
1 dog_r
array([[[160, 0, 0], [160, 0, 0], [160, 0, 0], ..., [113, 0, 0], [113, 0, 0], [112, 0, 0]], [[160, 0, 0], [160, 0, 0], [160, 0, 0], ..., [113, 0, 0], [113, 0, 0], [112, 0, 0]], [[160, 0, 0], [160, 0, 0], [160, 0, 0], ..., [113, 0, 0], [113, 0, 0], [112, 0, 0]], ..., [[165, 0, 0], [165, 0, 0], [165, 0, 0], ..., [212, 0, 0], [211, 0, 0], [210, 0, 0]], [[165, 0, 0], [165, 0, 0], [165, 0, 0], ..., [210, 0, 0], [210, 0, 0], [209, 0, 0]], [[164, 0, 0], [164, 0, 0], [164, 0, 0], ..., [209, 0, 0], [209, 0, 0], [209, 0, 0]]], dtype=uint8)
灰度图:
1 fig = plt.figure(figsize = (8,4)) 2 ax1 = fig.add_subplot(2,2, 1) 3 ax1.imshow(cgs, cmap="gray") 4 ax2 = fig.add_subplot(2,2, 2) 5 ax2.imshow(dgs, cmap='gray')
<matplotlib.image.AxesImage at 0x1fca2353358>
本章小结:
本章介绍了特征提取的常用方法,主要包括数值特征和文本特征,可以说不同的方法各有其优缺点。在任务起始阶段,应当尽可能多地尝试各种可能的提取方法,特征多不要紧,实际建模的时候,可以通过实验来筛选,但是少了就没有办法了,所以,在特征工程阶段,还是要多动脑筋,要提前考虑建模方案。因为一旦涉及海量数据,提取特征可是一个漫长的活,如果只是走一步看一步,效率就会大大降低。
做任务的时候,一定要结合论文,各种解决方案都要进行尝试,最好的方法就是先学学别人是怎么做的,再应用到自己的实际任务中。
第10章完。
该书资源下载,请至异步社区:https://www.epubit.com