Loading... ## 前言 Transformer模型源码解析过程参见前文, 这里是后续的实战示例. <div class="preview"> <div class="post-inser post box-shadow-wrap-normal"> <a href="https://zoe.red/2024/367.html" target="_blank" class="post_inser_a no-external-link no-underline-link"> <div class="inner-image bg" style="background-image: url(https://zoe.red/usr/uploads/2024/03/3146678007.png);background-size: cover;"></div> <div class="inner-content" > <p class="inser-title">Tranformer源码解读(Pytorch版本)</p> <div class="inster-summary text-muted"> 前言哈佛NLP团队实现的Pytorch版代码, 项目链接. 论文链接以下内容按照自己的理解过程一步步记录, 与源码... </div> </div> </a> <!-- .inner-content #####--> </div> <!-- .post-inser ####--> </div> ## 数据处理 ### 语料 这里使用对齐后的语料, 以下有几个语料数据集 | 名称 | 简介 | 下载链接 | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | AI Challenger 2017 | AI Challenger 2017中的英中机器文本翻译数据集,有超过1000万的英中对照的句子对作为数据集合,其中,训练集合占据绝大部分,为12904955对,验证集合8000对,测试集A 8000条,测试集B 8000条。 | | | News Commentary v12 | 新闻评论数据集, 包含语句对总数227568 | | | UN Parallel Corpus, UNv1.0 | 联合国平行语料库, 中英文语料1000w+ | https://conferences.unite.un.org/UNCorpus/Home/DownloadOverview<br />选择UNv1.0.6way.tar.gz.*的四个文件下载 | 其它翻译数据集搜索: 参见[链接](https://github.com/CLUEbenchmark/CLUEDatasetSearch?tab=readme-ov-file#%E6%9C%BA%E5%99%A8%E7%BF%BB%E8%AF%91) ### 分词器 英文分词器使用spacy库提供的, 中文分词器使用jieba库提供的 ### 数据预处理 将语料分别token化, 然后构建词汇表, 以及自动划分训练/验证/测试集. 实现代码 ```python """ 中文使用jieba分词器 英文使用spacy分词器 """ import jieba import spacy import os import random import torch from torchtext.vocab import build_vocab_from_iterator from tqdm.auto import tqdm # from tqdm import tqdm # from tqdm.contrib.concurrent import process_map from joblib import Parallel, delayed from multiprocessing import cpu_count def tokenizer_zh_(text, pbar=None): tokens = [token for token in jieba.cut(text, cut_all=False)] if pbar is not None: pbar.update(1) return tokens def load_tokenizers(): """ 初始化2个分词器接口函数 """ try: spacy_en = spacy.load("en_core_web_sm") except IOError: # 同上 os.system("python -m spacy download en_core_web_sm") spacy_en = spacy.load("en_core_web_sm") # tokenizer_en = lambda text: [token.text for token in spacy_en(text)] # noqa: E731 # tokenizer_zh = lambda text: [token for token in jieba.cut(text, cut_all=False)] # 精确分词模式 # noqa: E731 n_cpu = int(cpu_count()/2 *3) def tokenizer_en(texts): results = [] for doc in tqdm(spacy_en.pipe(texts, batch_size=64, n_process=-1), total=len(texts), desc='tokenizer_en'): results += [[token.text for token in doc]] return results def tokenizer_zh(texts): """ texts: 句子列表 """ results = Parallel(n_jobs=-1, verbose=0)(delayed(tokenizer_zh_)(_) for _ in tqdm(texts, desc='tokenizer_zh')) return results return tokenizer_en, tokenizer_zh def get_file_tokens(file_path, tokenizer, top_k=None, desc=''): """文件中每行一句话, 将每一行对应的token化结果返回 Args: file_path (_type_): 文本路径, 每行一句话 tokenizer (_type_): 分词器函数 top_k (_type_): 只取前n行结果 """ if not os.path.exists(file_path): print('文件路径异常:', file_path) return [] sentence_list = [] with open(file_path) as fin: lines = fin.readlines() lines = lines[:top_k] lines = [_.strip() for _ in lines] # for line in tqdm(lines, desc): # tokens = tokenizer(line.strip()) # sentence_list += [tokens] sentence_list = tokenizer(lines) return sentence_list def get_length_range(data): """ 返回数据全部item的长度范围 """ len_min, len_max = float('inf'), float('-inf') for _item in data: len_ = len(_item) len_min = min(len_min, len_) len_max = max(len_max, len_) return len_min, len_max def filter_with_rule(src, tgt): """ 设定规则, 过滤掉一些句子 分词后的句子长度<n被排除 """ min_token_count = 5 src_new = [] tgt_new = [] pass_count = 0 for _src, _tgt in tqdm(zip(src, tgt), 'progress:'): if len(_src) < min_token_count or len(_tgt) < min_token_count: pass_count += 1 else: src_new += [_src] tgt_new += [_tgt] print(f'完成, 排除掉: {pass_count}, 余下: {len(src_new)}') return src_new, tgt_new def build_vocabulary(src, tgt, min_freq=1): specials = ["<s>", "</s>", "<pad>", "<unk>"] # specials=["<pad>", "<unk>", "<s>", "</s>"] vocab_src = build_vocab_from_iterator(src, min_freq=min_freq, specials=specials,special_first=True) vocab_tgt = build_vocab_from_iterator(tgt, min_freq=min_freq, specials=specials,special_first=True) vocab_src.set_default_index(vocab_src["<unk>"]) vocab_tgt.set_default_index(vocab_tgt["<unk>"]) return vocab_src, vocab_tgt def data_split_with_indices(total, rate=(0.8, 0.1, 0.1)): """数据集划分 Args: total (_type_): 数据集总长度 rate (tuple, optional): 不同集合的比率. Defaults to (0.8, 0.1, 0.1). return: 不同数据集的索引 """ indices = list(range(total)) random.shuffle(indices) rate_total = sum(rate) rng_start = 0 split_result = [] for _rate in rate: num = int(_rate / rate_total * total + 0.5) rng_end = min(total, rng_start + num) split_result += [indices[rng_start: rng_end]] rng_start = rng_end return split_result def main(path_en, path_zh, save_path): tokenizer_en, tokenizer_zh = load_tokenizers() print('开始处理语料:') top_k = 100 top_k = None sentence_list_zh = get_file_tokens(path_zh, tokenizer_zh, top_k, desc='progress load & tokenize zh') sentence_list_en = get_file_tokens(path_en, tokenizer_en, top_k, desc='progress load & tokenize en') print() len_rng_en = get_length_range(sentence_list_en) len_rng_zh = get_length_range(sentence_list_zh) print('序列个数与序列长度信息:') print(f'en: sentence={len(sentence_list_en)} token_range={len_rng_en}') print(f'zh: sentence={len(sentence_list_zh)} token_range={len_rng_zh}') print() print('根据规则开始过滤序列') sentence_list_en, sentence_list_zh = filter_with_rule(sentence_list_en, sentence_list_zh) print('准备构建词词汇表') vocab_src, vocab_tgt = build_vocabulary(src=sentence_list_en, tgt=sentence_list_zh, min_freq=5) print(f'词汇表长度: en={len(vocab_src)} zh={len(vocab_tgt)}') print() total = len(sentence_list_en) datset_rate = [0.8, 0.1, 0.1] dataset_name = ['train', 'valid', 'test'] print('准备数据集切分') print(f'total={total}, name={dataset_name} rate={datset_rate}') split_indices_list = data_split_with_indices(total=total, rate=datset_rate) dataset_map = {} for name, indices in zip(dataset_name, split_indices_list): ds_src, ds_tgt = [ [ds[idx] for idx in indices] for ds in (sentence_list_en, sentence_list_zh) ] dataset_map[name] = [ds_src, ds_tgt] print(f'{name}={len(ds_src)}') params = { "total": total, # 构建词汇表前/数据集未划分前 句子对 总长度 "datset_rate": datset_rate, "dataset_name": dataset_name, "dataset_map": dataset_map, "vocab_src": vocab_src, "vocab_tgt": vocab_tgt, } torch.save(params, save_path) print('保存成功:', save_path) if __name__ == '__main__': file_path_en = 'raw/UNv1.0.6/UNv1.0.6way.en' file_path_zh = 'raw/UNv1.0.6/UNv1.0.6way.zh' save_path = 'UN-v1.0.en2zh.pt' # file_path_en = "raw/news-commentary/news-commentary-v12.zh-en.en" file_path_zh = "raw/news-commentary/news-commentary-v12.zh-en.zh" save_path = 'news-commentary-v12.en2zh.pt' # try: main(path_en=file_path_en, path_zh=file_path_zh, save_path=save_path) except Exception as e: print('异常信息:', e) ``` 输出示例 ```bash 开始处理语料: tokenizer_zh: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227603/227603 [00:04<00:00, 49051.39it/s] tokenizer_en: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227568/227568 [02:12<00:00, 1720.64it/s] 序列个数与序列长度信息: en: sentence=227568 token_range=(0, 157) zh: sentence=227603 token_range=(0, 452) 根据规则开始过滤序列 progress:: 227568it [00:00, 2167284.37it/s] 完成, 排除掉: 8359, 余下: 219209 准备构建词词汇表 词汇表长度: en=29084 zh=29132 准备数据集切分 total=219209, name=['train', 'valid', 'test'] rate=[0.8, 0.1, 0.1] train=175367 valid=21921 test=21921 保存成功: news-commentary-v12.en2zh.pt ``` 注意: 词汇表的建立依赖 `torchtext`库 * 安装脚本: `pip install -U torchtext` THE END 本文作者:将夜 本文链接:https://zoe.red/2024/379.html 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。 最后修改:2024 年 04 月 01 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏