自然语言处理通关手册--新词挖掘

新词挖掘

基于频次的新词挖掘

怎样的文本片段才算一个新词呢?不难想到的一个方案是,应用分词器对文本进行切分,观察是否存在一些高频的词汇或者词汇组合,并且它们在词典中并未出现过。可以把这些出现频数超过某个阈值的片段或者组合片断提取出来,作为该语料中的新词输出。
例如,对于文本“支持向量机是一类按监督学习方式对数据进行二元分类的广义线性分类器,其决策边界是对学习样本求解的最大边距超平面。铰链损失函数的思想就是让那些未能正确分类的和正确分类的之间的距离要足够的远。支持向量机使用铰链函数计算经验风险并在求解系统中加入了正则化项以优化结构风险,是一个具有稀疏性和稳健性的分类器。”,分词后发现一些在当前词典中没有且高频的词汇及词汇组合,如“支持”+“向量机”、“铰链”,那么“支持向量机”以及“铰链”就可以作为我们新发现的词汇。
这种简单粗暴的方式能否得到可靠的新词呢,下面通过一个实例进行演示。

# 首先对如下语料进行分词
import jieba
text = "支持向量机是一类按监督学习方式对数据进行二元分类的广义线性分类器,其决策边界是对学习样本求解的最大边距超平面。\
支持向量机使用铰链损失函数计算经验风险并在求解系统中加入了正则化项以优化结构风险,是一个具有稀疏性和稳健性的分类器。\
铰链损失函数的思想就是让那些未能正确分类的和正确分类的之间的距离要足够的远。\
支持向量机可以通过核方法进行非线性分类,是常见的核学习方法之一。\
支持向量机被提出于1964年,在二十世纪90年代后得到快速发展并衍生出一系列改进和扩展算法,\
在人像识别、文本分类等模式识别问题中有得到应用。"
words = jieba.lcut(text)

获取已有的中文词典:

def get_chinese_words(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        return [line.split()[0] for line in f.readlines()]


!wget -nc "https://labfile.oss.aliyuncs.com/courses/3205/chinese_words.txt"
CH_DICT = set(get_chinese_words("chinese_words.txt"))

对单词及词组进行频次统计(这里最多的组合词数计算至两阶):

import re

unigram_freq, bigram_freq = {}, {}
for i in range(len(words)-1):
    # 一阶计数,即单一单词,为未登录词且由中文构成
    if words[i] not in CH_DICT and not re.search("[^\u4e00-\u9fa5]", words[i]):
        if words[i] in unigram_freq:  # 一阶计数
            unigram_freq[words[i]] += 1
        else:
            unigram_freq[words[i]] = 1
    bigram = words[i]+words[i+1]
    # 二阶计数,即两个单词的组合形式,为未登录词且由中文构成
    if bigram not in CH_DICT and not re.search("[^\u4e00-\u9fa5]", bigram):
        if bigram in bigram_freq:
            bigram_freq[bigram] += 1
        else:
            bigram_freq[bigram] = 1


unigram_freq_sorted = sorted(
    unigram_freq.items(), key=lambda d: d[1], reverse=True)
bigram_freq_sorted = sorted(
    bigram_freq.items(), key=lambda d: d[1], reverse=True)

获取所有筛选过的一阶、二阶词汇,并且按照词频排序。

print("unigram:\n", unigram_freq_sorted)
print("bigram:\n", bigram_freq_sorted)

通过以上的计算结果,我们可以发现一些比较靠谱的新词,比如“边距”、“铰链损失”、“损失函数”,但大部分并不算得上是真正的新词。这是因为我们本身应用了工具 jieba 进行分词,而此分词方式又依赖于背后的词库,显然分词结果无法保证对于新词的切分。如果是针对大语料库,在此方法的基础上,还需要大量的人工筛选工作。接下来的内容中,我们来看一种更合理的新词挖掘方式。

基于自由疑固度以及左右邻字熵的新词挖掘

除了词频以外,我们还需要考虑以下两大方面作为候选新词的特征:
自由疑固度:表示一个字串的凝固程度。
左邻字熵与右邻字熵:表示一个字串左右搭配的丰富性。

所谓内部凝固度,用来衡量词搭配是否合理。比如,对于“的教育”、“教育家”这两个搭配,直观上讲“教育家”更为合理,即“教育”和“家”凝固地更紧一些,更像是一个单词。那么在计算机语言学中,如何来衡量这种“直观感受”呢?实际上,PMI(Pointwise Mutual Information,中文称为互信息)被用来度量词汇搭配的紧密度,定义如下( x,y 表示组成单词的两个成分):

image.png

若 PMI 高,即两个词共现的频率远大于两个词自由拼接的乘积概率,则说明这两个词组合在一块并非偶然出现,而是作为一种固定搭配。针对一个词有多种搭配组合,比如“教育家”可以由“教育”+“家”构成,也可以由“教”+“育家”构成,可以取其所有 PMI 中最大值作为内部凝固度。

而除了凝合程度,还需要从整体来看候选新词在语境中的表现。例如“混合”和“混凝”这两个候选新词,两者的内部凝固度都很高。而从左右搭配的丰富程度的角度来看,我们可以说“混合着”、“混合的”、“混合了”、“混合成”等,即在“混合”后面加各种各样的字;但“混凝”的用法却非常固定,除了“混凝土”,基本上后面不能加别的字了。因此可以认为,“混凝”并不单独成词,真正成词的其实是作为整体的“混凝土”。
自由运用程度,便可以一个候选词的左邻字与右邻字的丰富程度,即左右邻字熵来表达,公式如下,xi表示候选词w左(右)边出现的所有字:

image.png

给频数、内部凝固度与自由运用程度设定一个阈值,再基于词典去除原有的词汇,我们便可以找到新词了。

《红楼梦》一书中的新词挖掘实战

在接下来的部分中,我们将基于词频、自由疑固度以及左右邻字熵对《红楼梦》一书进行新词挖掘,主要步骤如下:
数据获取及预处理;词典获取。
将数据进行切分获取所有切分出的候选单词,并且统计词频信息、候选新词左右出现的字的信息。
根据第二步中统计的信息进行 PMI 值以及左右邻字熵的计算。
设定各指标的阈值,根据其值获取最终的新词结果。

首先读取数据:

import re


def preprocess_data(file_path):
    texts = []
    with open(file_path, "r", encoding="utf-8") as f:
        for text in f.readlines():
            text = re.sub("[^\u4e00-\u9fa5。?.,!:]", "",
                          text.strip())  # 只保留中文以及基本的标点符号
            text_splited = re.split("[。?.,!:]", text)  # 按照基本的标点符号进行分块
            texts += text_splited
    texts = [text for text in texts if text is not ""]  # 去除空字符
    return texts

处理数据《红楼梦》一书,按照基本的标点符号进行切分:

!wget -nc "https://labfile.oss.aliyuncs.com/courses/3205/hongloumeng.txt"
texts = preprocess_data("hongloumeng.txt")
texts

获取已有的中文词典:

def get_chinese_words(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        return [line.split()[0] for line in f.readlines()]


CH_DICT = set(get_chinese_words("chinese_words.txt"))

接下来需要对文本进行切分以及获取相关的频次信息,这里统一在一个函数 get_candidate_wordsinfo() 中,主要逻辑如下:
对文本按照一定的长度范围进行切分,切分出所有成词的可能性,这里称之为字符串。
对于所有切分出的字符串进行过滤,长度大于等于 2 的词以及不是词典 CH_DICT 中的词作为候选新词。
获取所有切分出的字符串的频次信息(在后续计算中需要用到一些字符串的频次信息)、候选新词词频信息、候选新词左右出现的字的统计信息。

def get_candidate_wordsinfo(texts, max_word_len):
    # texts 表示输入的所有文本,max_word_len 表示最长的词长
    # 四个词典均以单词为 key,分别以词频、词频、左字集合、右字集合为 value
    words_freq, candidate_words_freq, candidate_words_left_characters, candidate_words_right_characters = {}, {}, {}, {}
    WORD_NUM = 0  # 统计所有可能的字符串频次
    for text in texts:  # 遍历每个文本
        # word_indexes 中存储了所有可能的词汇的切分下标 (i,j) ,i 表示词汇的起始下标,j 表示结束下标,注意这里有包括了所有的字
        # word_indexes 的生成需要两层循环,第一层循环,遍历所有可能的起始下标 i;第二层循环,在给定 i 的情况下,遍历所有可能的结束下标 j
        word_indexes = [(i, j) for i in range(len(text))
                        for j in range(i + 1, i + 1 + max_word_len)]
        WORD_NUM += len(word_indexes)
        for index in word_indexes:  # 遍历所有词汇的下标
            word = text[index[0]:index[1]]  # 获取单词
            # 更新所有切分出的字符串的频次信息
            if word in words_freq:
                words_freq[word] += 1
            else:
                words_freq[word] = 1
            if len(word) >= 2 and word not in CH_DICT:  # 长度大于等于 2 的词以及不是词典中的词作为候选新词
                # 更新候选新词词频
                if word in candidate_words_freq:
                    candidate_words_freq[word] += 1
                else:
                    candidate_words_freq[word] = 1
                # 更新候选新词左字集合
                if index[0] != 0:  # 当为文本中首个单词时无左字
                    if word in candidate_words_left_characters:
                        candidate_words_left_characters[word].append(
                            text[index[0]-1])
                    else:
                        candidate_words_left_characters[word] = [
                            text[index[0]-1]]
                # 更新候选新词右字集合
                if index[1] < len(text)-1:  # 当为文本中末个单词时无右字
                    if word in candidate_words_right_characters:
                        candidate_words_right_characters[word].append(
                            text[index[1]+1])
                    else:
                        candidate_words_right_characters[word] = [
                            text[index[1]+1]]
    return WORD_NUM, words_freq, candidate_words_freq, candidate_words_left_characters, candidate_words_right_characters


WORD_NUM, words_freq, candidate_words_freq, candidate_words_left_characters, candidate_words_right_characters = \
    get_candidate_wordsinfo(texts=texts, max_word_len=3)  # 字符串最长为 3

在下一步中,计算 PMI 值以及左右邻字熵:

import math


# 计算候选单词的 PMI 值
def compute_pmi(words_freq, candidate_words):
    words_pmi = {}
    for word in candidate_words:
        # 首先,将某个候选单词按照不同的切分位置切分成两项,比如“电影院”可切分为“电”和“影院”以及“电影”和“院”
        bi_grams = [(word[0:i], word[i:]) for i in range(1, len(word))]
        # 对所有切分情况计算 PMI 值,取最大值作为当前候选词的最终 PMI 值
        # words_freq[bi_gram[0]],words_freq[bi_gram[1]] 分别表示一个候选儿童村新词的前后两部分的出现频次
        words_pmi[word] = max(map(lambda bi_gram: math.log(
            words_freq[word]/(words_freq[bi_gram[0]]*words_freq[bi_gram[1]]/WORD_NUM)), bi_grams))
        """
        通俗版本
        pmis = []
        for bi_gram in bigrams: # 遍历所有切分情况
            pmis.append(math.log(words_freq[word]/(words_freq[bi_gram[0]]*words_freq[bi_gram[1]]/WORD_NUM))) # 计算 pmi 值
        words_pmi[word] = max(pmis) # 取最大值
        """
    return words_pmi


words_pmi = compute_pmi(words_freq, candidate_words_freq)
words_pmi

计算候选单词的邻字熵:

from collections import Counter


def compute_entropy(candidate_words_characters):
    words_entropy = {}
    for word, characters in candidate_words_characters.items():
        character_freq = Counter(characters)  # 统计邻字的出现分布
        # 根据出现分布计算邻字熵
        words_entropy[word] = sum(map(
            lambda x: - x/len(characters) * math.log(x/len(characters)), character_freq.values()))
    return words_entropy


words_left_entropy = compute_entropy(candidate_words_left_characters)
words_right_entropy = compute_entropy(candidate_words_right_characters)

最后,根据各指标阈值获取最终的新词结果:

def get_newwords(candidate_words_freq, words_pmi, words_left_entropy, words_right_entropy,
                 words_freq_limit=15, pmi_limit=6, entropy_limit=1):
    # 在每一项指标中根据阈值进行筛选
    candidate_words = [
        k for k, v in candidate_words_freq.items() if v >= words_freq_limit]
    candidate_words_pmi = [k for k, v in words_pmi.items() if v >= pmi_limit]
    candidate_words_left_entropy = [
        k for k, v in words_left_entropy.items() if v >= entropy_limit]
    candidate_words_right_entropy = [
        k for k, v in words_right_entropy.items() if v >= entropy_limit]
    # 对筛选结果进行合并
    return list(set(candidate_words).intersection(candidate_words_pmi, candidate_words_left_entropy, candidate_words_right_entropy))


get_newwords(candidate_words_freq, words_pmi,
             words_left_entropy, words_right_entropy)

调节阈值:

get_newwords(candidate_words_freq, words_pmi, words_left_entropy,
             words_right_entropy, words_freq_limit=100, pmi_limit=3, entropy_limit=3)

从以上结果可看出,基于自由疑固度以及左右邻字熵的新词挖掘相较于单纯的基于频次的方法,效果较好些,比如能够将一些人名“黛玉”、“麝月”、“李纨”等识别出,但还是存在一些非单词,例如“ 不过是”、“的丫头”、“宝玉的”、“了宝玉”,“了一个”等,大家可以观察这些“词汇”,思考如何解决此问题(提示:stopword,词性)。

upload-labs通关手册包含了几种绕过文件上传限制的方法。其中方法一是通过修改文件头来绕过上传限制,方法二是制作图片马并利用文件包含漏洞来上传文件。 另外,还可以通过验证MIME-Type代码来绕过上传限制,这可以通过查看源代码分析,利用$_FILE['upload_file']['type']获取上传文件的MIME-Type类型,并通过修改MIME-Type类型来绕过验证。 此外,还可以使用Burpsuite工具来截断HTTP请求并修改MIME-Type类型来绕过验证。 还有一种方法是通过查看源码来了解上传文件的验证机制,例如通过检查文件扩展名来判断是否允许上传,可以通过修改允许上传的文件类型来绕过验证。 以上是一些绕过文件上传限制的方法,可以在upload-labs通关手册中找到更详细的解释和示例。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [文件上传-uploadlab通关手册](https://blog.youkuaiyun.com/qq_51153463/article/details/124547943)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [upload-labs通关手册](https://blog.youkuaiyun.com/weixin_44800419/article/details/107979400)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

辣椒种子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值