分析 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 的角度处理数据

统计学的角度处理数据的方法

从统计学的角度考虑主要是单词的频率,数据的次数等等,这里考虑的问题很多,总结来说就是,每种方法的处理套路是,使用分词,然后使用词干提取的方法,将所有的单词进行词干归一化处理。注意,这里是从统计的角度考虑问题,那么就不要考虑到单词的时态问题,词干提取可以更好的表示出现的频率。

主要使用的方法总结,之后我会具体说一下使用的比较复杂的方法:

  1. Not 类,统计一句话中 ‘not’ 出现的次数
  2. WordMatchShare 类,两句话中均出现的单词的占所有单词的比例
  3. TFIDFWordMatchShare 类,与前面的类似,只不过这里加权了文件的出现次数
  4. Length 类,表示长度的一些数据
  5. LengthDiff 类表示问题的长度之差
  6. LengthDiffRate 类,数据上比较长短
  7. PowerfulWord 这个类是为后面服务的,计算数据中词语的影响力
  8. PowerfulWordOneSide 类,考虑单词出现的比例以及正确的比例
  9. PowerfulWordDoubleSideRate 类,考虑两边都出现的单词的比例,以及这些单词对应的 label 的比例
  10. TFIDF 类,使用 sklearn 中方法直接获取 TFIDF 类
  11. DulNum 类,计算完全重复的问题的数量
  12. EnCharCount 类,统计每句话中字母的出现的频率,
  13. NgramJaccardCoef 类,使用 ngram 的方法计算两个问题之间的距离
  14. NgramDiceDistance 类,与上面的方法类似,只是计算距离的方法不同
  15. Distance 类,这里是下面的方法父类,是用于计算句子之间距离的工具
  16. NgramDistance 类,这个主要是在上面的基础上,结合矩阵的距离的方法
  17. 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
posted @ 2019-07-01 17:16  虾野百鹤  阅读(719)  评论(0编辑  收藏  举报