2021SC@SDUSC
数据预处理
BERT是一种预训练语言模型,因此在介绍它之前,先简单说一下什么是nlp中的预训练过程。
其实在nlp的下游任务(比如机器翻译、阅读理解等)中,可以使用的样本数据是比较少的,因为这些任务需要的都是经过专门标注的数据,这样就使得拿这些样本数据直接训练出来的模型效果比较一般,因此就需要对模型进行预训练。
预训练的目的就是,提前训练好这些下游任务中底层的、共性的部分模型,然后再用下游任务各自的样本数据来训练各自的模型,这样就可以极大地加快收敛速度
- 预训练的目的
对于nlp的下游任务,尽管它们的最终目标各不相同,但是它们也有着共同的、也是必须首先要做的东西,那就是要让模型理解文档中的单词和句子。
具体是将文本中的无法直接计算的单词转变为可以计算的向量或者矩阵等形式,并且这些数字化的向量要能够比较好地反映出对应单词在句子中的含义。
在nlp中采用的是语言模型来做预训练,从最初的word embedding到ELMO、再到GPT,以及现在的BERT,其实做的都是上面说的这件事,只不过效果变得越来越好。
文章目录
词干与词形化(NLTK库,Porter Stemming算法)
自然语言处理领域,我们遇到了两个或两个以上单词具有共同根源的情况。 例如,agreed,agreeing 和 agreeable这三个词具有相同的词根。 涉及任何这些词的搜索应该把它们当作是根词的同一个词。 因此将所有单词链接到它们的词根变得非常重要。 NLTK库有一些方法来完成这个链接,并给出显示根词的输出。
from nltk.stem.porter import PorterStemmer
stemmer = PorterStemmer()
NLTK提供三种常见的词干提取器接口,如下所示
'''基于Porter词干提取算法'''
from nltk.stem.porter import PorterStemmer
porter_stemmer = PorterStemmer()
porter_stemmer.stem(‘multiply’) # u’multipli’
''' 基于Lancaster 词干提取算法 '''
from nltk.stem.lancaster import LancasterStemmer
lancaster_stemmer = LancasterStemmer()
lancaster_stemmer.stem(‘multiply’) # ‘multiply’
'''基于Snowball 词干提取算法 ''''
from nltk.stem import SnowballStemmer
snowball_stemmer = SnowballStemmer(“english”)
文件中方法具体实现
clean_phrase
该方法要实现的功能:删除空的、重复的和标点符号,使用小写
def clean_phrase(Keyphrases):
phrase_set = set()
return_phrases = []
for phrase in Keyphrases:
if len(phrase) > 0:
clean_phrase = lower(remove_punc(phrase))
if len(clean_phrase) > 0 and clean_phrase not in phrase_set:
return_phrases.append(clean_phrase.split())
phrase_set.add(clean_phrase)
输入一个keyphrases,对于关键词中的每个phrase进行遍历,如果存在则进行删除空的、重复的和标点符号的操作,然后再进行lower case。
方法中涉及到的方法:
def lower(text):
return text.lower()
def remove_punc(words):
strings = ' '.join(words)
return strings.strip('.').strip(',').strip('?').strip()
split_vdom(VDOM)方法
代码如下(示例):
def split_vdom(VDOM):
'''used in function `refactor_text_vdom` '''
# convert string to json
VDOMs = json.loads(VDOM)
word2block = {}
block_words = []
block_features = []
for bi, block in enumerate(VDOMs):
block_words.append(block['text'].split())
block_features.append(block['feature'])
for wi in range(block['start_idx'], block['end_idx']):
word2block[wi] = bi
assert len(block_words) == len(block_features)
return word2block, block_words, block_features
其中涉及到的函数方法:
- 将字符串转化为json
导入json
import json
使用json.loads()函数即可,传入字符串,输出json格式
# convert string to json
VDOMs = json.loads(VDOM)
- 将json转化为字符串
使用 json.dumps() 函数,必要时需要传入ensure_ascii=False, indent=2参数
#json文件
[{'name': '冯振振', 'age': '23', 'job': 'Python engineer', 'motto': 'I like coding'}, {'name': '康康', 'age': '23', 'job': 'web engineer', 'motto': '专业前端,不至于前端'}]
new_string = json.dumps(json_list,ensure_ascii=False)
# 输入的结果如下
[{"name": "冯振振", "age": "23", "job": "Python engineer", "motto": "I like coding"}, {"name": "康康", "age": "23", "job": "web engineer", "motto": "专业前端,不至于前端"}]
# 或者
new_string = json.dumps(json_list,ensure_ascii=False,indent=2)
#输出结果如下
[
{
"name": "冯振振",
"age": "23",
"job": "Python engineer",
"motto": "I like coding"
},
{
"name": "康康",
"age": "23",
"job": "web engineer",
"motto": "专业前端,不至于前端"
}
]
- python中enumerate()函数的用法
enumerate多用于在for循环中得到计数,利用它可以同时获得索引和值,即index和value。
for bi, block in enumerate(VDOMs):
block_words.append(block['text'].split())
block_features.append(block['feature'])
- python中assert的用法
assert 表达式 [, 参数]
当表达式为真时,程序继续往下执行;
当表达式为假时,抛出AssertionError错误,并将 参数 输出
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n
foo('0')
# 代码执行结果
# AssertionError: n is zero!
在该方法中的用法:
assert len(block_words) == len(block_features)
refactor_text_vdom()
代码如下(示例):
def refactor_text_vdom(text, VDOM):
shuffler = DEL_ASCII()
words = text.split()
word2block, block_words, block_features = split_vdom(VDOM)
doc_words = []
new_word2block = []
for wi, w in enumerate(words):
if shuffler.do(w.strip()):
doc_words.append(w.strip())
bi = word2block[wi]
if w in block_words[bi]:
new_word2block.append(bi)
else:
new_word2block.append(-1)
logger.info('Error ! the word not found in block')
assert len(doc_words) == len(new_word2block)
return doc_words, new_word2block, block_features
相关函数用法:
- split()
split() 通过指定分隔符对字符串进行切片,
如果参数 num 有指定值,则分隔 num+1 个子字符串
# 函数语法:
str.split(str="", num=string.count(str)).
# 用法示例
str = "Line1-abcdef \nLine2-abc \nLine4-abcd";
print str.split( ); # 以空格为分隔符,包含 \n
print str.split(' ', 1 ); # 以空格为分隔符,分隔成两个
# 输出结果如下
['Line1-abcdef', 'Line2-abc', 'Line4-abcd']
['Line1-abcdef', '\nLine2-abc \nLine4-abcd']
#示例二
txt = "Google#Runoob#Taobao#Facebook"
# 第二个参数为 1,返回两个参数列表
x = txt.split("#", 1)
print x
norm_doc_to_char(word_list):
作用:在函数中被调用,用于(word_list)查找答案。
def norm_doc_to_char(word_list):
norm_char = unicodedata.normalize('NFD', " ".join(word_list))
stem_char = " ".join([stemmer.stem(w.strip()) for w in norm_char.split(" ")])
return norm_char, stem_char
norm_phrase_to_char(phrase_list):
作用:在函数中被调用,用于(phrase_list)查找答案。
删除相同的关键字短语和空短语
def norm_phrase_to_char(phrase_list):
norm_phrases = set()
for phrase in phrase_list:
p = " ".join([w.strip() for w in phrase if len(w.strip()) > 0])
if len(p) < 1:continue
norm_phrases.add(unicodedata.normalize('NFD', p))
norm_stem_phrases = []
for norm_chars in norm_phrases:
stem_chars = " ".join([stemmer.stem(w) for w in norm_chars.split(" ")])
norm_stem_phrases.append((norm_chars, stem_chars))
return norm_stem_phrases
find_stem_answer(word_list, ans_list)
- 调用以上两种方法
norm_doc_char, stem_doc_char = norm_doc_to_char(word_list)
norm_stem_phrase_list = norm_phrase_to_char(ans_list)
判断单词是否在查找到的列表当中
tot_ans_str = []
tot_start_end_pos = []
for norm_ans_char, stem_ans_char in norm_stem_phrase_list:
norm_stem_doc_char = " ".join([norm_doc_char, stem_doc_char])
if norm_ans_char not in norm_stem_doc_char and stem_ans_char not in norm_stem_doc_char:
continue
else:
norm_doc_words = norm_doc_char.split(" ")
stem_doc_words = stem_doc_char.split(" ")
norm_ans_words = norm_ans_char.split(" ")
stem_ans_words = stem_ans_char.split(" ")
assert len(norm_doc_words) ==len(stem_doc_words)
assert len(norm_ans_words) == len(stem_ans_words)
进行定位,找到关键词并且标注它所在的位置
tot_pos = []
for i in range(0, len(stem_doc_words) - len(stem_ans_words) + 1):
Flag = False
if norm_ans_words == norm_doc_words[i:i+len(norm_ans_words)]:
Flag = True
elif stem_ans_words == norm_doc_words[i:i+len(stem_ans_words)]:
Flag = True
elif norm_ans_words == stem_doc_words[i:i+len(norm_ans_words)]:
Flag = True
elif stem_ans_words == stem_doc_words[i:i+len(stem_ans_words)]:
Flag = True
if Flag:
tot_pos.append([i, i+len(norm_ans_words)-1])
assert (i+len(stem_ans_words)-1) >= i
实现函数的功能,找到关键词后,并将关键词的开始和结束位置进行位置标记
if len(tot_pos) > 0 :
tot_start_end_pos.append(tot_pos)
tot_ans_str.append(norm_ans_char.split())
assert len(tot_ans_str) == len(tot_start_end_pos)
assert len(word_list) == len(norm_doc_char.split(" "))
if len(tot_ans_str) == 0:
return None
return {'keyphrases':tot_ans_str, 'start_end_pos':tot_start_end_pos}
original dataset loader
加载openkp数据集:
def openkp_loader(mode, source_dataset_dir):
''' load source OpenKP dataset :'url', 'VDOM', 'text', 'KeyPhrases' '''
logger.info("start loading %s data ..." % mode)
source_path = os.path.join(source_dataset_dir, 'OpenKP%s.jsonl' % mode)
data_pairs = []
with codecs.open(source_path, "r", "utf-8") as corpus_file:
for idx, line in enumerate(tqdm(corpus_file)):
json_ = json.loads(line)
data_pairs.append(json_)
return data_pair
加载kp20k数据集:
注解:加载源Kp20k数据集:“标题”、“摘要”、“关键字”
返回:元组:src\u字符串,trg\u字符串
def kp20k_loader(mode, source_dataset_dir,
src_fields = ['title', 'abstract'],
trg_fields = ['keyword'], trg_delimiter=';'):
logger.info("start loading %s data ..." % mode)
source_path = os.path.join(source_dataset_dir, 'kp20k_%s.json' % mode)
data_pairs = []
with codecs.open(source_path, "r", "utf-8") as corpus_file:
for idx, line in enumerate(tqdm(corpus_file)):
json_ = json.loads(line)
trg_strs = []
src_str = '.'.join([json_[f] for f in src_fields])
[trg_strs.extend(re.split(trg_delimiter, json_[f])) for f in trg_fields]
data_pairs.append((src_str, trg_strs))
return data_pairs
总结
以上方法实现了对数据的简单清洗,包括去除空格,标点符号,进行小写形式规范。