目录
死亡不是生命的终点,被遗忘才是
—— 24.12.21
一、新词发现
引言
假设没有词表,如何从文本中发现新词?
随着时间的推移,新词会不断地出现,固有词表会过时
补充词表有利于下游任务
词相当于一种固定搭配
1.新词发现的衡量标准
① 内部稳固
词的内部应该是稳固的,用内部稳固度 / 互信息衡量
内部稳固度/互信息:词语中几个字的固定搭配出现的次数除以词语中每个字单独出现的概率的乘积
公式:
n:词语中字的个数,词语的长度;
p(W):词语中几个字的固定搭配词语的出现次数;
p(c1)…p(cn):词语中每个字在词表中单独出现的概率
② 外部多变
词的外部应该是多变的,用左右熵衡量
左右熵: 将词语外部出现的所有字再除以出现的总词频数,得到出现某个字的频率pi,代入公式进行求和后取反,得到词语两边的左右熵,词语的外部两侧出现一个固定字的频率应该较低,换句话说,词的外部应该是多变的,而不是固定的,左右熵的值大小可以衡量词的外部值是否多变,左右熵的值越大,词的外部越多变
公式:
n:词语中字的个数,词语的长度;
pi:词语后出现某个字的频率,词语外部出现的所有字除以出现的总词频数
用两个指标计算候选词的分数,根据分数衡量候选词是否是新词
2.步骤
① 先选取一个固定长度
② 找到文本中所有固定长度字数的潜在候选词
③ 对每个候选词计算候选词的内部稳固度和左右熵两指标
④ 根据两指标的分数进行排序
⑤ 筛选出认为最有可能是词的候选词
3.代码实现
Ⅰ、初始化
defaultdict():Python 标准库 collections
模块中的一个类,它是内置 dict
类的子类。与普通字典不同的是,defaultdict
可以在键不存在时自动为其提供一个默认值,避免了因访问不存在的键而引发 KeyError
异常。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
default_factory |
可调用对象(如函数、类)或 None |
None |
否 | 当访问不存在的键时,用于生成默认值的可调用对象。如果传入 None ,其行为与普通字典相同,访问不存在的键会引发 KeyError 。常见的传入类型有 int 、list 、set 等,分别会在键不存在时返回默认的整数 0 、空列表 [] 、空集合 set() 。 |
**kwargs |
关键字参数 | 无 | 否 | 用于初始化字典的键值对,用法和普通字典的初始化相同。 |
def __init__(self, corpus_path):
# 设置词语最高长度 1 - 5 四个字的词语
self.max_word_length = 5
self.word_count = defaultdict(int)
self.left_neighbor = defaultdict(dict)
self.right_neighbor = defaultdict(dict)
self.load_corpus(corpus_path)
self.calc_pmi()
self.calc_entropy()
self.calc_word_values()
Ⅱ、加载语料数据并统计
open():Python 的内置函数,用于打开文件并返回一个文件对象。通过这个文件对象,你可以对文件进行读取、写入、追加等操作。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
file |
str 或 pathlib.Path 对象 |
无 | 是 | 要打开的文件的路径,可以是绝对路径或相对路径。 |
mode |
str | 'r' |
否 | 文件打开模式,常见的模式有: - 'r' :只读模式,用于读取文件内容。 - 'w' :写入模式,若文件存在则清空内容,不存在则创建新文件。 - 'a' :追加模式,在文件末尾追加内容,不存在则创建新文件。 - 'x' :独占创建模式,创建新文件,若文件已存在则报错。 - 'b' :二进制模式,可与其他模式组合,如 'rb' 表示二进制只读。 - 't' :文本模式,默认值,可与其他模式组合,如 'wt' 表示文本写入。 - '+' :更新模式,可与其他模式组合,如 'r+' 表示读写模式。 |
buffering |
int | -1 | 否 | 缓冲策略: - -1 :使用系统默认缓冲策略。 - 0 :不使用缓冲(仅适用于二进制模式)。 - 1 :行缓冲(仅适用于文本模式)。 - 大于 1 的整数:指定缓冲区大小(字节)。 |
encoding |
str | 取决于系统 | 否 | 文件的编码方式,如 'utf-8' 、'gbk' 等。处理非 ASCII 字符的文本文件时需指定。 |
errors |
str | None |
否 | 编码错误处理方式,如 'ignore' 忽略非法字符,'replace' 用 ? 替换,'strict' 遇到非法字符抛出异常。 |
newline |
str | None |
否 | 控制换行符处理,None 表示根据系统自动处理,'' 不进行转换,还可指定 '\n' 、'\r' 等。 |
closefd |
bool | True |
否 | 是否在关闭文件对象时同时关闭文件描述符,一般保持默认。 |
opener |
可调用对象 | None |
否 | 自定义文件打开器,高级用法,一般不用。 |
strip():Python 字符串对象的方法,用于移除字符串首尾指定的字符序列(默认为空格、制表符、换行符等空白字符)。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
chars |
str | None |
否 | 要移除的字符序列。如果不提供该参数,则默认移除字符串首尾的空白字符。 |
#加载语料数据,并进行统计
def load_corpus(self, path):
with open(path, encoding="utf8") as f:
for line in f:
sentence = line.strip()
for word_length in range(1, self.max_word_length):
self.ngram_count(sentence, word_length)
return
Ⅲ、 取词,并记录左邻右邻出现过的字及次数
按照指定的窗口长度(word_length
)从输入的句子(sentence
)中提取词语,并统计这些词语的出现次数,同时记录每个词语的左邻字符和右邻字符及其出现的次数。
# 按照窗口长度取词,并记录左邻右邻出现过的字及次数
def ngram_count(self, sentence, word_length):
# 遍历句子,提取长度为 word_length 的词语
for i in range(len(sentence) - word_length + 1):
# 从句子中截取长度为 word_length 的词语
word = sentence[i:i + word_length]
# 统计该词语的出现次数,若词语不在 word_count 字典中,初始值为 0
self.word_count[word] += 1
# 检查当前词语是否有左邻字符
if i - 1 >= 0:
# 获取左邻字符
char = sentence[i - 1]
# 统计左邻字符的出现次数,若左邻字符不在对应词语的 left_neighbor 字典中,初始值为 0
self.left_neighbor[word][char] = self.left_neighbor[word].get(char, 0) + 1
# 检查当前词语是否有右邻字符
if i + word_length < len(sentence):
# 获取右邻字符
char = sentence[i + word_length]
# 统计右邻字符的出现次数,若右邻字符不在对应词语的 right_neighbor 字典中,初始值为 0
self.right_neighbor[word][char] = self.right_neighbor[word].get(char, 0) + 1
return
Ⅳ、计算左右熵
values():Python 字典(dict
)对象的一个方法,用于返回一个包含字典中所有值的视图对象。这个视图对象会动态反映字典的变化,即当字典中的值被修改、添加或删除时,视图对象也会相应地更新。
items():Python 字典对象的一个方法,用于返回一个包含字典中所有键值对的视图对象。每个键值对以元组的形式表示,元组的第一个元素是键,第二个元素是对应的值。同样,这个视图对象也是动态的,会随着字典的变化而更新。
#计算熵
def calc_entropy_by_word_count_dict(self, word_count_dict):
total = sum(word_count_dict.values())
entropy = sum([-(c / total) * math.log((c / total), 10) for c in word_count_dict.values()])
return entropy
#计算左右熵
def calc_entropy(self):
self.word_left_entropy = {}
self.word_right_entropy = {}
for word, count_dict in self.left_neighbor.items():
self.word_left_entropy[word] = self.calc_entropy_by_word_count_dict(count_dict)
for word, count_dict in self.right_neighbor.items():
self.word_right_entropy[word] = self.calc_entropy_by_word_count_dict(count_dict)
Ⅴ、统计词长下的词总数
defaultdict():Python 标准库 collections
模块中的一个类,它继承自内置的 dict
类。defaultdict
的特殊之处在于,当访问一个不存在的键时,它会自动为该键创建一个默认值,而不是像普通字典那样抛出 KeyError
异常。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
default_factory |
可调用对象(如函数、类)或 None |
None |
否 | 当访问字典中不存在的键时,会调用该可调用对象来生成默认值。若为 None ,行为同普通字典,访问不存在的键会抛出 KeyError 。常见传入类型有 int (默认值 0 )、list (默认值 [] )、set (默认值 set() )等。 |
**kwargs |
关键字参数 | 无 | 否 | 用于初始化字典的键值对,和普通字典初始化时传入键值对的用法相同。 |
items():字典(包括 defaultdict
)对象的一个方法,用于返回一个包含字典中所有键值对的视图对象。每个键值对以元组的形式表示,元组的第一个元素是键,第二个元素是对应的值。这个视图对象是动态的,意味着当字典内容发生变化时,视图对象也会相应更新。
#统计每种词长下的词总数
def calc_total_count_by_length(self):
self.word_count_by_length = defaultdict(int)
for word, count in self.word_count.items():
self.word_count_by_length[len(word)] += count
return
Ⅵ、计算互信息/凝固度
#计算互信息(pointwise mutual information 凝固度)
def calc_pmi(self):
self.calc_total_count_by_length()
self.pmi = {}
for word, count in self.word_count.items():
p_word = count / self.word_count_by_length[len(word)]
p_chars = 1
for char in word:
p_chars *= self.word_count[char] / self.word_count_by_length[1]
self.pmi[word] = math.log(p_word / p_chars, 10) / len(word)
return
Ⅶ、 根据互信息、左右熵计算分数并排序
get():字典(dict
)的 get()
函数用于获取字典中指定键对应的值。与使用方括号 []
直接访问键值不同的是,当指定的键不存在于字典中时,get()
函数不会抛出 KeyError
异常,而是返回一个默认值,这样能让代码更具健壮性。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
key |
任意不可变类型(如 int 、str 、tuple 等) |
无 | 是 | 要在字典中查找的键。因为字典使用哈希表存储键值对,而不可变类型能保证哈希值稳定,所以键必须是不可变类型。 |
default |
任意类型 | None |
否 | 当指定的 key 不在字典中时,get() 函数返回的默认值。用户可根据需求自定义该值,例如传入空列表 [] 、数字 0 等。 |
def calc_word_values(self):
self.word_values = {}
for word in self.pmi:
if len(word) < 2 or "," in word:
continue
pmi = self.pmi.get(word, 1e-3)
le = self.word_left_entropy.get(word, 1e-3)
re = self.word_right_entropy.get(word, 1e-3)
# 通过三个指标综合评估词的价值
# self.word_values[word] = pmi + le + re
# self.word_values[word] = pmi * min(le, re)
self.word_values[word] = pmi * max(le, re)
# self.word_values[word] = pmi + le * re
# self.word_values[word] = pmi * le * re
Ⅷ、新词发现的衡量标准
import math
from collections import defaultdict
class NewWordDetect:
def __init__(self, corpus_path):
# 设置词语最高长度 1 - 5 四个字的词语
self.max_word_length = 5
self.word_count = defaultdict(int)
self.left_neighbor = defaultdict(dict)
self.right_neighbor = defaultdict(dict)
self.load_corpus(corpus_path)
self.calc_pmi()
self.calc_entropy()
self.calc_word_values()
#加载语料数据,并进行统计
def load_corpus(self, path):
with open(path, encoding="utf8") as f:
for line in f:
sentence = line.strip()
for word_length in range(1, self.max_word_length):
self.ngram_count(sentence, word_length)
return
#按照窗口长度取词,并记录左邻右邻出现过的字及次数
def ngram_count(self, sentence, word_length):
for i in range(len(sentence) - word_length + 1):
word = sentence[i:i + word_length]
self.word_count[word] += 1
if i - 1 >= 0:
char = sentence[i - 1]
self.left_neighbor[word][char] = self.left_neighbor[word].get(char, 0) + 1
if i + word_length < len(sentence):
char = sentence[i +word_length]
self.right_neighbor[word][char] = self.right_neighbor[word].get(char, 0) + 1
return
#计算熵
def calc_entropy_by_word_count_dict(self, word_count_dict):
total = sum(word_count_dict.values())
entropy = sum([-(c / total) * math.log((c / total), 10) for c in word_count_dict.values()])
return entropy
#计算左右熵
def calc_entropy(self):
self.word_left_entropy = {}
self.word_right_entropy = {}
for word, count_dict in self.left_neighbor.items():
self.word_left_entropy[word] = self.calc_entropy_by_word_count_dict(count_dict)
for word, count_dict in self.right_neighbor.items():
self.word_right_entropy[word] = self.calc_entropy_by_word_count_dict(count_dict)
#统计每种词长下的词总数
def calc_total_count_by_length(self):
self.word_count_by_length = defaultdict(int)
for word, count in self.word_count.items():
self.word_count_by_length[len(word)] += count
return
#计算互信息(pointwise mutual information 凝固度)
def calc_pmi(self):
self.calc_total_count_by_length()
self.pmi = {}
for word, count in self.word_count.items():
p_word = count / self.word_count_by_length[len(word)]
p_chars = 1
for char in word:
p_chars *= self.word_count[char] / self.word_count_by_length[1]
self.pmi[word] = math.log(p_word / p_chars, 10) / len(word)
return
def calc_word_values(self):
self.word_values = {}
for word in self.pmi:
if len(word) < 2 or "," in word:
continue
pmi = self.pmi.get(word, 1e-3)
le = self.word_left_entropy.get(word, 1e-3)
re = self.word_right_entropy.get(word, 1e-3)
# 通过三个指标综合评估词的价值
# self.word_values[word] = pmi + le + re
# self.word_values[word] = pmi * min(le, re)
self.word_values[word] = pmi * max(le, re)
# self.word_values[word] = pmi + le * re
# self.word_values[word] = pmi * le * re
if __name__ == "__main__":
nwd = NewWordDetect("sample_corpus.txt")
value_sort = sorted([(word, count) for word, count in nwd.word_values.items()], key=lambda x:x[1], reverse=True)
print([x for x, c in value_sort if len(x) == 2][:10])
print([x for x, c in value_sort if len(x) == 3][:10])
print([x for x, c in value_sort if len(x) == 4][:10])
二、挑选重要词
从词到理解
有了分词能力后,需要利用词来完成对文本的理解
首先可以想到的,就是从文章中挑选重要词
1.何为重要词
假如一个词在某类文本(假设为A类)中出现次数很多,而在其他类别文本(非A类)出现很少,那么这个词是A类文本的重要词(高权重词)
例:恒星、黑洞 ——> 天文
反之,如果一个词出现在很多领域,则其对于任意类别的重要性都很差
例: