分析 Kaggle TOP0.1% 如何处理文本数据
感觉大佬的代码写的就是好,在处理数据的方面,首先定义一个 提取特征的类, class Extractor(object):
,然后每一种方法对这个类进行重构,这个类主要结构就是:
class Extractor(object):
def __init__(self, config_fp):
# set feature name
self.feature_name = self.__class__.__name__
# set feature file path
self.data_feature_fp = None
# load configuration file
self.config = configparser.ConfigParser()
self.config.read(config_fp)
# 这个函数什么都没写,就是用来重构的,表示特征的个数
def get_feature_num(self):
assert False, 'Please override function: Extractor.get_feature_num()'
# assert False 后面加字符串就是为了解释哪里出错的,给出错误信息
def extract_row(self, row):
assert False, 'Please override function: Extractor.extract_row()'
# 抽取原始数据的特征
def extract(self, data_set_name, part_num=1, part_id=0):
"""
Extract the feature from original data set
:param data_set_name: name of data set
:param part_num: number of partitions of data
:param part_id: partition ID which will be extracted
:return:
"""
接下来看如何具体的从统计的角度与 NLP 的角度处理数据
统计学的角度处理数据的方法
从统计学的角度考虑主要是单词的频率,数据的次数等等,这里考虑的问题很多,总结来说就是,每种方法的处理套路是,使用分词,然后使用词干提取的方法,将所有的单词进行词干归一化处理。注意,这里是从统计的角度考虑问题,那么就不要考虑到单词的时态问题,词干提取可以更好的表示出现的频率。
主要使用的方法总结,之后我会具体说一下使用的比较复杂的方法:
- Not 类,统计一句话中 ‘not’ 出现的次数
- WordMatchShare 类,两句话中均出现的单词的占所有单词的比例
- TFIDFWordMatchShare 类,与前面的类似,只不过这里加权了文件的出现次数
- Length 类,表示长度的一些数据
- LengthDiff 类表示问题的长度之差
- LengthDiffRate 类,数据上比较长短
- PowerfulWord 这个类是为后面服务的,计算数据中词语的影响力
- PowerfulWordOneSide 类,考虑单词出现的比例以及正确的比例
- PowerfulWordDoubleSideRate 类,考虑两边都出现的单词的比例,以及这些单词对应的 label 的比例
- TFIDF 类,使用 sklearn 中方法直接获取 TFIDF 类
- DulNum 类,计算完全重复的问题的数量
- EnCharCount 类,统计每句话中字母的出现的频率,
- NgramJaccardCoef 类,使用 ngram 的方法计算两个问题之间的距离
- NgramDiceDistance 类,与上面的方法类似,只是计算距离的方法不同
- Distance 类,这里是下面的方法父类,是用于计算句子之间距离的工具
- NgramDistance 类,这个主要是在上面的基础上,结合矩阵的距离的方法
- InterrogativeWords 类,这个主要是统计疑问句的比例
TFIDFWordMatchShare 方法
TFIDFWordMatchShare 这个方法是考虑单词出现在文件的次数,也就是 IDF 的意思,然后用这个加权来表示共同出现的单词的加权的文件的比例,这里具体看下重点的代码就是:
def init_idf(data):
idf = {}
# 先统计了单词的 IDF 值
num_docs = len(data)
for word in idf:
idf[word] = math.log(num_docs / (idf[word] + 1.)) / math.log(2.)
# 这里是一个对数中的换底公式
LogUtil.log("INFO", "idf calculation done, len(idf)=%d" % len(idf))
# 返回一个字典,是整个文件中的单词的,平均每个单词出现在文件中的次数,
# 比如说,5个文件,这个单词一共出现了三次,那么就是 5/3
return idf
def extract_row(self, row):
qwords = {}
# 这里是先统计了单词出现的次数, qword来计算,
# 下面的这个公式计算的是, 同时出现在两个问题中的单词他们所加权的文件总数
# 比如说,上面前面计算 IDF 是对于整个文件来说,单词 'word'的idf值是 5/3,那么对于这一句话来说,'word' 出现了两次,并且 # 'word' 在两个问题均出现,那么这个值就是 10 /3 ,然后对于每个出现的单词计算就可以了
sum_shared_word_in_q1 = sum([q1words[w] * self.idf.get(w, 0) for w in q1words if w in q2words])
sum_shared_word_in_q2 = sum([q2words[w] * self.idf.get(w, 0) for w in q2words if w in q1words])
sum_tol = sum(q1words[w] * self.idf.get(w, 0) for w in q1words) + sum(
q2words[w] * self.idf.get(w, 0) for w in q2words)
if 1e-6 > sum_tol:
return [0.]
else:
return [1.0 * (sum_shared_word_in_q1 + sum_shared_word_in_q2) / sum_tol]
PowerfulWord 方法
这个方法是统计单词的权重及比例,
def generate_powerful_word(data, subset_indexs):
"""
计算数据中词语的影响力,格式如下:
词语 --> [0. 出现语句对数量,1. 出现语句对比例,2. 正确语句对比例,3. 单侧语句对比例,4. 单侧语句对正确比例,
5. 双侧语句对比例,6. 双侧语句对正确比例]
"""
words_power = {}
train_subset_data = data.iloc[subset_indexs, :]
# 取出 subset_indexs中的所有的行
# 然后遍历 subset_indexs 中的所有的行
class PowerfulWordDoubleSide(Extractor):
# 通过设置阈值来提取这句话中关键的单词,然后组成单词向量
def init_powerful_word_dside(pword, thresh_num, thresh_rate):
pword_dside = []
# 出现语句对的数量乘以双侧语句对的比例,得到双侧语句对的数量,
# 计算两边都出现,并且比例高的单词,然后从大到小排序
pword = filter(lambda x: x[1][0] * x[1][5] >= thresh_num, pword)
# 抽取出 pword 中满足条件的所有项目
pword_sort = sorted(pword, key=lambda d: d[1][6], reverse=True)
# 表示按照降序排序
pword_dside.extend(map(lambda x: x[0], filter(lambda x: x[1][6] >= thresh_rate, pword_sort)))
# 在list的结尾追加一序列的值
LogUtil.log('INFO', 'Double side power words(%d): %s' % (len(pword_dside), str(pword_dside)))
return pword_dside
句子之间的距离的计算
由于之前自己对句子之间的距离的了解也比较少,所以这里写的详细一点,这里的主要思想是
class Distance(Extractor):
def __init__(self, config_fp, distance_mode):
Extractor.__init__(self, config_fp)
self.feature_name += '_%s' % distance_mode
self.valid_distance_mode = ['edit_dist', 'compression_dist']
# 这两个方法是计算两个字符串之间的距离使用的,其中 'edit_dist' 直接调用的一个方法,而'compression_dist'通过简单计算
# 就可以得到
assert distance_mode in self.valid_distance_mode, "Wrong aggregation_mode: %s" % distance_mode
# 初始化参数
self.distance_mode = distance_mode
# 使用不同的方法来计算字符串之间的距离
self.distance_func = getattr(DistanceUtil, self.distance_mode)
def extract_row(self, row):
q1 = str(row['question1']).strip()
q2 = str(row['question2']).strip()
q1_stem = ' '.join([snowball_stemmer.stem(word).encode('utf-8') for word in
nltk.word_tokenize(TextPreProcessor.clean_text(str(row['question1']).decode('utf-8')))])
q2_stem = ' '.join([snowball_stemmer.stem(word).encode('utf-8') for word in
nltk.word_tokenize(TextPreProcessor.clean_text(str(row['question2']).decode('utf-8')))])
# 先分词,然后将单词用空格连成句子,假装是句子
return [self.distance_func(q1, q2), self.distance_func(q1_stem, q2_stem)]
def get_feature_num(self):
return 2
class NgramDistance(Distance):
# 这里没有构造函数,子类直接调用父类的构造函数
def extract_row(self, row):
# 使用词干提取
fs = list()
aggregation_modes_outer = ["mean", "max", "min", "median"]
aggregation_modes_inner = ["mean", "std", "max", "min", "median"]
# 这些主要是 np 中矩阵的一些方法,用于数据处理均值,最大值,最小值,中位数,矩阵的标准差 等等
for n_ngram in range(1, 4):
q1_ngrams = NgramUtil.ngrams(q1_words, n_ngram)
q2_ngrams = NgramUtil.ngrams(q2_words, n_ngram)
val_list = list()
for w1 in q1_ngrams:
_val_list = list()
for w2 in q2_ngrams:
s = self.distance_func(w1, w2)
# 两个句子在 ngram 下面的距离,然后存起来
_val_list.append(s)
if len(_val_list) == 0:
_val_list = [MISSING_VALUE_NUMERIC]
val_list.append(_val_list)
if len(val_list) == 0:
val_list = [[MISSING_VALUE_NUMERIC]]
# val_list 存的就是在 q1_ngrams 下每两个句子之间的距离,组成一个矩阵
for mode_inner in aggregation_modes_inner:
tmp = list()
for l in val_list:
tmp.append(MathUtil.aggregate(l, mode_inner))
fs.extend(MathUtil.aggregate(tmp, aggregation_modes_outer))
return fs
def get_feature_num(self):
return 4 * 5
NLP 角度处理数据的方法
这里主要考虑的是解析树的构成,构建一颗解析树,从语句的解析树提取句子的特征,
def init_tree_properties(tree_fp):
features = {}
f = open(tree_fp)
for line in f:
[qid, json_s] = line.split(' ', 1)
# 分割一次,分成两个
features[qid] = []
root = -1
parent = {}
indegree = {}
# calculate in-degree and find father
# 删除开头与结尾的空格
if 0 < len(json_s.strip()):
tree_obj = json.loads(json_s)
# 将一个JSON编码的字符串转换回一个Python数据结构:
# 返回一个字典
assert len(tree_obj) <= 1
tree_obj = tree_obj[0]
for k, r in sorted(tree_obj.items(), key=lambda x: int(x[0]))[1:]:
if r['word'] is None:
continue
head = int(r['head'])
k = int(k)
if 0 == head:
root = k
parent[k] = head
indegree[head] = indegree.get(head, 0) + 1
# calculate number of leaves
n_child = 0
for i in parent:
if i not in indegree:
n_child += 1
# calculate the depth of a tree
depth = 0
for i in parent:
if i not in indegree:
temp_id = i
temp_depth = 0
while (temp_id in parent) and (0 != parent[temp_id]):
temp_depth += 1
temp_id = parent[temp_id]
depth = max(depth, temp_depth)
# calculate the in-degree of root
n_root_braches = indegree.get(root, 0)
# calculate the max in-degree
n_max_braches = 0
if 0 < len(indegree):
n_max_braches = max(indegree.values())
features[str(qid)] = [n_child, depth, n_root_braches, n_max_braches]
f.close()
return features