当前位置:   article > 正文

李宏毅机器学习HW5-Transformer代码解析(解释代码并记录跑HW5的历程)_李宏毅hw5

李宏毅hw5

 前言:淋过雨,才会想为别人撑伞,本次hw相比前面几次hw难度大很多,并且在网络上无法提交获得test的得分,所以就以解释代码为主,但解释代码也很困难,我看了好几天才把整个代码看懂,接下来将会详细解释代码,也会解释网络架构,并提供要彻底搞懂本次作业所需要学习的部分。

首先简要介绍一下本次作业的整体框架:

        一、数据预处理

                1.下载解压数据 2.处理符号空格等 3.分割训练集验证集 4.分词化单元 5.二进制化数据集

        二、加载数据

                1.生成TranslationTask 2.用task加载数据 3.定义数据批迭代器

        三、模型架构(使用fairseq库)

                1.RNNencoder 2.AttentionLayer 3.RNNdecoder 4.seq2seq 5.定义model_build函数

        四、损失函数和优化器定义

                1.Loss func-标签平滑交叉熵 2.optimizer- NoamOpt

        五、定义训练步骤

                就是正常正向传播再反向传播更新参数,但是其中用了scaler来进行梯度计算和参数更新(nlp问题容易出现梯度爆炸和梯度消失等问题,使用scaler来处理精度问题)

        六、定义推理预测以及验证

                1.inference函数 2.定义valid

        七、模型训练

               

准备:

        需要先提前下载好数据,我做的时候是通过网上整理好的资料获得的数据集,也可以通过wget,这个的话要自己操作一下。

        我的作业是从这个视频的链接下载的

         (强推)李宏毅2021/2022春机器学习课程_哔哩哔哩_bilibili

        注意一下,我在做这个作业的时候,得到的fairseq库是有问题的,需要更换别的版本的库,可以在bash里面输入下面的进行下载,其中xxxx需要改为自己的端口号。git国内不能用,记得魔法上网。

  1. set https_proxy=http://127.0.0.1:xxxx
  2. set http_proxy=http://127.0.0.1:xxxx
  3. pip install git+https://github.com/One-sixth/fairseq.git

        建议在本地的jupyter中跑的时候加上一下代码,调试时显示的信息就是中文

  1. !chcp 65001
  2. !echo 中文

        接下来略过导入库和设置随机种子的代码部分,到数据下载的部分

  1. data_dir = './DATA/rawdata'
  2. dataset_name = 'ted2020'
  3. urls = (
  4. '"https://onedrive.live.com/download?cid=3E549F3B24B238B4&resid=3E549F3B24B238B4%214989&authkey=AGgQ-DaR8eFSl1A"',
  5. '"https://onedrive.live.com/download?cid=3E549F3B24B238B4&resid=3E549F3B24B238B4%214987&authkey=AA4qP_azsicwZZM"',
  6. # # If the above links die, use the following instead.
  7. # "https://www.csie.ntu.edu.tw/~r09922057/ML2021-hw5/ted2020.tgz",
  8. # "https://www.csie.ntu.edu.tw/~r09922057/ML2021-hw5/test.tgz",
  9. # # If the above links die, use the following instead.
  10. # "https://mega.nz/#!vEcTCISJ!3Rw0eHTZWPpdHBTbQEqBDikDEdFPr7fI8WxaXK9yZ9U",
  11. # "https://mega.nz/#!zNcnGIoJ!oPJX9AvVVs11jc0SaK6vxP_lFUNTkEcK2WbxJpvjU5Y",
  12. )
  13. file_names = (
  14. 'ted2020.tgz', # train & dev
  15. 'test.tgz', # test
  16. )
  17. prefix = Path(data_dir).absolute() / dataset_name
  18. prefix.mkdir(parents=True, exist_ok=True)
  19. for u, f in zip(urls, file_names):
  20. path = prefix/f
  21. if not path.exists():
  22. if 'mega' in u:
  23. !megadl {u} --path {path}
  24. else:
  25. !wget {u} -O {path}
  26. if path.suffix == ".tgz":
  27. !tar -xvf {path} -C {prefix}
  28. elif path.suffix == ".zip":
  29. !unzip -o {path} -d {prefix}
  30. !mv {prefix/'raw.en'} {prefix/'train_dev.raw.en'}
  31. !mv {prefix/'raw.zh'} {prefix/'train_dev.raw.zh'}
  32. !mv {prefix/'test.en'} {prefix/'test.raw.en'}
  33. !mv {prefix/'test.zh'} {prefix/'test.raw.zh'}

这里就是下载数据,并把数据文件重命名,最后四行mv是linux下的操作命令,我们可以自己手动更改或者用操作命令行更改

  1. src_lang = 'en'
  2. tgt_lang = 'zh'
  3. data_prefix = f'{prefix}/train_dev.raw'
  4. test_prefix = f'{prefix}/test.raw'
  5. !head {data_prefix+'.'+src_lang} -n 5
  6. !head {data_prefix+'.'+tgt_lang} -n 5

这里是显示train_dev.raw和test.raw文件的前五个sequence,最后两行head是linux下的操作命令,我们可以改为分别用来显示全文,windows没有head显示文件前五行的操作,只能显示全文,最好分开显示,这一步是有助于你理解数据内部格式,和数据是做什么的,后面你也会继续用该操作来显示数据格式

  1. !type E:\Li_Hung_Yi_ml\HW5_2021\DATA\rawdata\ted2020\train_dev.raw.en
  2. !type E:\Li_Hung_Yi_ml\HW5_2021\DATA\rawdata\ted2020\test.raw.zh

接下来是对字符做标点符号处理即clean_s函数和clean_corpus函数

其中,strQ2B函数是全角转半角操作,用的utf-8格式字符12288指全角空格转32半角空格,然后减去65248是指英文字符全角转半角

clean_s函数有两个参数,s和lang,s是sequence,lang是language,即en和zh,使用re对sequence进行符号操作,可能最困惑的是sub这个函数里面的几个参数,例如r"[()]",这个是python中的正则表达式,将会用""空字符替换掉括号内的内容。详细的看下面解释:

这段代码是使用Python中的正则表达式模块`re`来执行字符串替换操作。具体来说,它使用了`re.sub()`函数,该函数用于在字符串中查找匹配某个正则表达式模式的部分,并将其替换为指定的内容。

让我们来解释代码中的各个部分:

1. `re.sub()`是`re`模块中的一个函数,用于执行正则表达式的替换操作。

2. `r"[()]"`是一个正则表达式模式。让我们分解它:
   - `\(` 匹配一个左括号 `(`。
   - `[^()]*` 匹配零个或多个不包含左括号和右括号的字符。这个部分用于匹配括号中的任何文本。
   - `\)` 匹配一个右括号 `)`。
   所以整个模式可以匹配任何形式的括号及其内部文本,例如 `(text)`。

3. `""`是替换字符串,表示将匹配到的内容替换为空字符串,即删除匹配到的部分。

所以,这段代码的作用是从字符串`s`中删除所有括号及其内部的文本,即删除所有形如 `(text)` 的内容。 

clean_corpus是调用clean_s对文件的en版本和zh版本所有seq进行处理,并改名为xx.clean.xx,其中的ratio,max_len,min_len等参数是对处理进行限制

  1. import re
  2. def strQ2B(ustring):
  3. """Full width -> half width"""
  4. # reference:https://ithelp.ithome.com.tw/articles/10233122
  5. ss = []
  6. for s in ustring:
  7. rstring = ""
  8. for uchar in s:
  9. inside_code = ord(uchar)
  10. if inside_code == 12288: # Full width space: direct conversion
  11. inside_code = 32
  12. elif (inside_code >= 65281 and inside_code <= 65374): # Full width chars (except space) conversion
  13. inside_code -= 65248
  14. rstring += chr(inside_code)
  15. ss.append(rstring)
  16. return ''.join(ss)
  17. def clean_s(s, lang):
  18. if lang == 'en':
  19. s = re.sub(r"\([^()]*\)", "", s) # remove ([text])
  20. s = s.replace('-', '') # remove '-'
  21. s = re.sub('([.,;!?()\"])', r' \1 ', s) # keep punctuation
  22. elif lang == 'zh':
  23. s = strQ2B(s) # Q2B
  24. s = re.sub(r"\([^()]*\)", "", s) # remove ([text])
  25. s = s.replace(' ', '')
  26. s = s.replace('—', '')
  27. s = s.replace('“', '"')
  28. s = s.replace('”', '"')
  29. s = s.replace('_', '')
  30. s = re.sub('([。,;!?()\"~「」])', r' \1 ', s) # keep punctuation
  31. s = ' '.join(s.strip().split())
  32. return s
  33. def len_s(s, lang):
  34. if lang == 'zh':
  35. return len(s)
  36. return len(s.split())
  37. def clean_corpus(prefix, l1, l2, ratio=9, max_len=1000, min_len=1):
  38. if Path(f'{prefix}.clean.{l1}').exists() and Path(f'{prefix}.clean.{l2}').exists():
  39. print(f'{prefix}.clean.{l1} & {l2} exists. skipping clean.')
  40. return
  41. with open(f'{prefix}.{l1}', 'r') as l1_in_f:
  42. with open(f'{prefix}.{l2}', 'r') as l2_in_f:
  43. with open(f'{prefix}.clean.{l1}', 'w') as l1_out_f:
  44. with open(f'{prefix}.clean.{l2}', 'w') as l2_out_f:
  45. for s1 in l1_in_f:
  46. s1 = s1.strip()
  47. s2 = l2_in_f.readline().strip()
  48. s1 = clean_s(s1, l1)
  49. s2 = clean_s(s2, l2)
  50. s1_len = len_s(s1, l1)
  51. s2_len = len_s(s2, l2)
  52. if min_len > 0: # remove short sentence
  53. if s1_len < min_len or s2_len < min_len:
  54. continue
  55. if max_len > 0: # remove long sentence
  56. if s1_len > max_len or s2_len > max_len:
  57. continue
  58. if ratio > 0: # remove by ratio of length
  59. if s1_len/s2_len > ratio or s2_len/s1_len > ratio:
  60. continue
  61. print(s1, file=l1_out_f)
  62. print(s2, file=l2_out_f)

接下来一个部分是对数据进行划分,把train_dev.raw.clean.en和train_dev.raw.clean.zh划分为train data和valid data

利用shuffle来对index进行打乱,然后根据train_ratio和打乱后的index来分配原文件内的sequence进入新的train和valid文件,最终生成了四个新文件如下图

  1. if (prefix/f'train.clean.{src_lang}').exists() \
  2. and (prefix/f'train.clean.{tgt_lang}').exists() \
  3. and (prefix/f'valid.clean.{src_lang}').exists() \
  4. and (prefix/f'valid.clean.{tgt_lang}').exists():
  5. print(f'train/valid splits exists. skipping split.')
  6. else:
  7. line_num = sum(1 for line in open(f'{data_prefix}.clean.{src_lang}'))
  8. labels = list(range(line_num))
  9. random.shuffle(labels)
  10. for lang in [src_lang, tgt_lang]:
  11. train_f = open(os.path.join(data_dir, dataset_name, f'train.clean.{lang}'), 'w')
  12. valid_f = open(os.path.join(data_dir, dataset_name, f'valid.clean.{lang}'), 'w')
  13. count = 0
  14. for line in open(f'{data_prefix}.clean.{lang}', 'r'):
  15. if labels[count]/line_num < train_ratio:
  16. train_f.write(line)
  17. else:
  18. valid_f.write(line)
  19. count += 1
  20. train_f.close()
  21. valid_f.close()

接下来是进行分词化,把一个单词变成分词形式,用spm库中的SentencePieceTrainer架构train这四个文件中出现的中文和英文词汇,最后train出一个spm模型和一个spm分词字典库

  1. import sentencepiece as spm
  2. vocab_size = 8000
  3. if (prefix/f'spm{vocab_size}.model').exists():
  4. print(f'{prefix}/spm{vocab_size}.model exists. skipping spm_train.')
  5. else:
  6. spm.SentencePieceTrainer.train(
  7. input=','.join([f'{prefix}/train.clean.{src_lang}',
  8. f'{prefix}/valid.clean.{src_lang}',
  9. f'{prefix}/train.clean.{tgt_lang}',
  10. f'{prefix}/valid.clean.{tgt_lang}']),
  11. model_prefix=prefix/f'spm{vocab_size}',
  12. vocab_size=vocab_size,
  13. character_coverage=1,
  14. model_type='unigram', # 'bpe' works as well
  15. input_sentence_size=1e6,
  16. shuffle_input_sentence=True,
  17. normalization_rule_name='nmt_nfkc_cf',
  18. )

下面的代码是用train好的spm8000.model来对train,valid,test文件进行分词化,然后output放在新的文件里面即最后的out_f,tok为spm_model对每个sequence的输出,用print到file的操作来实现写入文件

  1. spm_model = spm.SentencePieceProcessor(model_file=str(prefix/f'spm{vocab_size}.model'))
  2. in_tag = {
  3. 'train': 'train.clean',
  4. 'valid': 'valid.clean',
  5. 'test': 'test.raw.clean',
  6. }
  7. for split in ['train', 'valid', 'test']:
  8. for lang in [src_lang, tgt_lang]:
  9. out_path = prefix/f'{split}.{lang}'
  10. if out_path.exists():
  11. print(f"{out_path} exists. skipping spm_encode.")
  12. else:
  13. with open(prefix/f'{split}.{lang}', 'w') as out_f:
  14. with open(prefix/f'{in_tag[split]}.{lang}', 'r') as in_f:
  15. for line in in_f:
  16. line = line.strip()
  17. tok = spm_model.encode(line, out_type=str)
  18. print(' '.join(tok), file=out_f)

下面的代码是利用fairseq库中的函数对文件内的内容进行二值化,并生成两个词汇表(en和zh的),但是由于用了joined-dictionary这个参数,en和zh共享词汇表

  1. binpath = Path('./DATA/data-bin', dataset_name)
  2. if binpath.exists():
  3. print(binpath, "exists, will not overwrite!")
  4. else:
  5. !python -m fairseq_cli.preprocess \
  6. --source-lang {src_lang}\
  7. --target-lang {tgt_lang}\
  8. --trainpref {prefix/'train'}\
  9. --validpref {prefix/'valid'}\
  10. --testpref {prefix/'test'}\
  11. --destdir {binpath}\
  12. --joined-dictionary\
  13. --workers 2

我们看一下词汇表的内容,例如_,是分词,875945是出现的频次,而每个分词的index则是比如说‘s’是6,‘的’是8,为什么要加3呢?因为其中<unk>,<s>,<\s>,<pad>四个特殊空白字符在前面作为前四个。

 接下来创建TranslationTask,利用task加载数据,可能会有疑问说为什么没有传入数据文件的绝对路径,但是就能加载数据呢?因为前面进行二值化的时候用的是fairseq库的函数,处理生成的文件会有固定格式的文件名,如test.en-zh.en.bin,test.en-zh.en.idx,task的load_dataset函数会根据split,自动加上‘en-zh’和后缀,那为什么是en-zh呢,因为在task创建的时候,进行了配置,source_lang和target_lang这边定义了en和-zh,配置的一个参数train_subset是指训练的时候用split为train加载的数据训练,具体的想了解这个工作的原理可以看库文件里函数的代码,required_seq_len_multiple是指产生的seq的长度需要为该倍数,比如原seq长度为11,要为8的倍数,就会加上<s>或<\s>或<pad>,总共5个

  1. from fairseq.tasks.translation import TranslationConfig, TranslationTask
  2. ## setup task
  3. task_cfg = TranslationConfig(
  4. data=config.datadir,
  5. source_lang=config.source_lang,
  6. target_lang=config.target_lang,
  7. train_subset="train",
  8. required_seq_len_multiple=8,
  9. dataset_impl="mmap",
  10. upsample_primary=1,
  11. )
  12. task = TranslationTask.setup_task(task_cfg)
  1. logger.info("loading data for epoch 1")
  2. task.load_dataset(split="train", epoch=1, combine=True) # combine if you have back-translation data.
  3. task.load_dataset(split="valid", epoch=1)

下面这段代码是向你展示task的数据集案例

  1. sample = task.dataset("valid")[1]
  2. pprint.pprint(sample)
  3. pprint.pprint(
  4. "Source: " + \
  5. task.source_dictionary.string(
  6. sample['source'],
  7. config.post_process,
  8. )
  9. )
  10. pprint.pprint(
  11. "Target: " + \
  12. task.target_dictionary.string(
  13. sample['target'],
  14. config.post_process,
  15. )
  16. )

输出如下,pprint是用漂亮的格式进行输出,尽可能保存了数据原有的格式,然后其中举个例子29对应着‘在’,而在字典中,‘在’的对应行数是26,这也应证了我前面说的,你不妨自己试一下,把target的tensor里面的第一个元素分别改为0,1,2,3会出现什么情况

  1. {'id': 1,
  2. 'source': tensor([ 18, 15, 6, 2154, 64, 19, 75, 5, 334, 14, 340, 1346,
  3. 1627, 7, 2]),
  4. 'target': tensor([ 149, 675, 29, 269, 41, 161, 1088, 646, 592, 366, 3105, 2345,
  5. 1374, 208, 2])}
  6. "Source: that's exactly what i do optical mind control ."
  7. 'Target: 這實在就是我所做的--光學操控思想'

下面是定义数据批迭代器函数,并向你展示迭代器的用法,首先创建一个多轮数据批迭代器集合,然后用next_epoch_itr来得到不同训练轮数的数据批迭代器,然后利用next,来获得该迭代器中的不同批数据,load_dataset_iterator中的max_tokens参数是指每个sequence中的最大标签数,小于大于该标签数的就舍弃

  1. def load_data_iterator(task, split, epoch=1, max_tokens=4000, num_workers=1, cached=True):
  2. batch_iterator = task.get_batch_iterator(
  3. dataset=task.dataset(split),
  4. max_tokens=max_tokens,
  5. max_sentences=None,
  6. max_positions=utils.resolve_max_positions(
  7. task.max_positions(),
  8. max_tokens,
  9. ),
  10. ignore_invalid_inputs=True,
  11. seed=seed,
  12. num_workers=num_workers,
  13. epoch=epoch,
  14. disable_iterator_cache=not cached,
  15. # Set this to False to speed up. However, if set to False, changing max_tokens beyond
  16. # first call of this method has no effect.
  17. )
  18. return batch_iterator
  19. demo_epoch_obj = load_data_iterator(task, "valid", epoch=1, max_tokens=20, num_workers=1, cached=False)
  20. demo_iter = demo_epoch_obj.next_epoch_itr(shuffle=True)
  21. sample = next(demo_iter)
  22. sample

以上就是数据预处理和准备部分,接下来是更重要的模型架构

 了解RNNencoder模型架构,你需要有的知识是,word_embedding,word2vec,RNN,GRU,LSTM,encoder

下面是视频链接,如果不能魔法上网,可以去b站找对应资料

ML Lecture 14: Unsupervised Learning - Word Embedding - YouTube

ML Lecture 21-1: Recurrent Neural Network (Part I) - YouTube

【機器學習2021】Transformer (上) - YouTube

【機器學習2021】Transformer (下) - YouTube

我直接开始讲下面代码的forward部分,并解释前面的定义部分,

x = self.embed_tokens(src_tokens)这个是会把source_tokens输入embedding layer当中,假如source_tokens是(312,18)这样的dimension的,你可以认为312是batch_size,18是这个batch_size的sequence_len,假如word2vec的vec_dimension是256,然后输出会是(312,18,256),就是把sequence中的每个word都变为一个256维的vector来代表这个word,具体这个embedding_layer长什么样,要看传入的embed_tokens这个参数给的是什么样的layer,稍微讲解一下embedding_layer(可以跳过),词嵌入是有好几种方法,有根据上下文频率来确定词与词之间的相似度进而确定词嵌入matrix的,也有根据预测来确定词嵌入matrix的,这里面有glove,cbow,skip-gram等方法,可以深入了解

x = self.embed_tokens(src_tokens)

 接下来,定义h0,x.new_zeros这个的意思是,使用x一样数据类型,一样device的设置来生成一个随机初始化的参数矩阵,后面的参数是该参数矩阵的size,为什么要乘2,因为GRU定义的时候bidirectional这里定义为True,双向的门控单元,然后下一句的意思是用h0初始化GRU的hidden_dim weight,然后得到一个新的输出,和hidden_layer的weight,

  1. h0 = x.new_zeros(2 * self.num_layers, bsz, self.hidden_dim)
  2. x, final_hiddens = self.rnn(x, h0)

下面这个是用来做什么的呢?是得到source_token的pad,然后使其为true,以便后续处理方便

encoder_padding_mask = src_tokens.eq(self.padding_idx).t()

 最终RNNencoder的输出是(bsz,256,1024)维,256是vec的dim,1024是hidden_layer*2

  1. class RNNEncoder(FairseqEncoder):
  2. def __init__(self, args, dictionary, embed_tokens):
  3. super().__init__(dictionary)
  4. self.embed_tokens = embed_tokens
  5. self.embed_dim = args.encoder_embed_dim
  6. self.hidden_dim = args.encoder_ffn_embed_dim
  7. self.num_layers = args.encoder_layers
  8. self.dropout_in_module = nn.Dropout(args.dropout)
  9. self.rnn = nn.GRU(
  10. self.embed_dim,
  11. self.hidden_dim,
  12. self.num_layers,
  13. dropout=args.dropout,
  14. batch_first=False,
  15. bidirectional=True
  16. )
  17. self.dropout_out_module = nn.Dropout(args.dropout)
  18. self.padding_idx = dictionary.pad()
  19. def combine_bidir(self, outs, bsz: int):
  20. out = outs.view(self.num_layers, 2, bsz, -1).transpose(1, 2).contiguous()
  21. return out.view(self.num_layers, bsz, -1)
  22. def forward(self, src_tokens, **unused):
  23. bsz, seqlen = src_tokens.size()
  24. # get embeddings
  25. x = self.embed_tokens(src_tokens)
  26. x = self.dropout_in_module(x)
  27. # B x T x C -> T x B x C
  28. x = x.transpose(0, 1)
  29. # pass thru bidirectional RNN
  30. h0 = x.new_zeros(2 * self.num_layers, bsz, self.hidden_dim)
  31. x, final_hiddens = self.rnn(x, h0)
  32. outputs = self.dropout_out_module(x)
  33. # outputs = [sequence len, batch size, hid dim * directions]
  34. # hidden = [num_layers * directions, batch size , hid dim]
  35. # Since Encoder is bidirectional, we need to concatenate the hidden states of two directions
  36. final_hiddens = self.combine_bidir(final_hiddens, bsz)
  37. # hidden = [num_layers x batch x num_directions*hidden]
  38. encoder_padding_mask = src_tokens.eq(self.padding_idx).t()
  39. #print(encoder_padding_mask)
  40. #print(encoder_padding_mask.size())
  41. return tuple(
  42. (
  43. outputs, # seq_len x batch x hidden
  44. final_hiddens, # num_layers x batch x num_directions*hidden
  45. encoder_padding_mask, # seq_len x batch
  46. )
  47. )
  48. def reorder_encoder_out(self, encoder_out, new_order):
  49. # This is used by fairseq's beam search. How and why is not particularly important here.
  50. return tuple(
  51. (
  52. encoder_out[0].index_select(1, new_order),
  53. encoder_out[1].index_select(1, new_order),
  54. encoder_out[2].index_select(1, new_order),
  55. )
  56. )

下一个是AttentionLayer,该层是decoder中的一个层,我们直接讲forward,inputs参数是decoder经过其中的embedding层然后得到的输出,这当中,inputs是key,encoder_outputs是query和value,output_embed_dim是value,要经过一个input_proj是把inputs升维,以和query相乘即bmm,然后用encoder_padding_mask对attn中的pad位为负无穷,接着对最后一个维度做softmax,再与v相乘,再增加输入的位置信息,再通过一个线性层得到输出

  1. class AttentionLayer(nn.Module):
  2. def __init__(self, input_embed_dim, source_embed_dim, output_embed_dim, bias=False):
  3. super().__init__()
  4. self.input_proj = nn.Linear(input_embed_dim, source_embed_dim, bias=bias)
  5. self.output_proj = nn.Linear(
  6. input_embed_dim + source_embed_dim, output_embed_dim, bias=bias
  7. )
  8. def forward(self, inputs, encoder_outputs, encoder_padding_mask):
  9. # inputs: T, B, dim
  10. # encoder_outputs: S x B x dim
  11. # padding mask: S x B
  12. # convert all to batch first
  13. inputs = inputs.transpose(1,0) # B, T, dim
  14. encoder_outputs = encoder_outputs.transpose(1,0) # B, S, dim
  15. encoder_padding_mask = encoder_padding_mask.transpose(1,0) # B, S
  16. # project to the dimensionality of encoder_outputs
  17. x = self.input_proj(inputs)
  18. # compute attention
  19. # (B, T, dim) x (B, dim, S) = (B, T, S)
  20. attn_scores = torch.bmm(x, encoder_outputs.transpose(1,2))
  21. # cancel the attention at positions corresponding to padding
  22. if encoder_padding_mask is not None:
  23. # leveraging broadcast B, S -> (B, 1, S)
  24. encoder_padding_mask = encoder_padding_mask.unsqueeze(1)
  25. attn_scores = (
  26. attn_scores.float()
  27. .masked_fill_(encoder_padding_mask, float("-inf"))
  28. .type_as(attn_scores)
  29. ) # FP16 support: cast to float and back
  30. # softmax on the dimension corresponding to source sequence
  31. attn_scores = F.softmax(attn_scores, dim=-1)
  32. # shape (B, T, S) x (B, S, dim) = (B, T, dim) weighted sum
  33. x = torch.bmm(attn_scores, encoder_outputs)
  34. # (B, T, dim)
  35. x = torch.cat((x, inputs), dim=-1)
  36. x = torch.tanh(self.output_proj(x)) # concat + linear + tanh
  37. # restore shape (B, T, dim) -> (T, B, dim)
  38. return x.transpose(1,0), attn_scores

这个相对来说是最复杂的,但也没有那么复杂,关键看forward,其中,如encoder的forward,最后的out是打包成一个tuple的,对于incremental_state暂时不需要管,这个是在代码运行过程中库函数产生的参数,在valid的时候会调用之前训练好的weight,总体的流程就是输入一个embedding层,然后做attention输入GRU,然后将输出通过线性层转化为8000维的输出

  1. class RNNDecoder(FairseqIncrementalDecoder):
  2. def __init__(self, args, dictionary, embed_tokens):
  3. super().__init__(dictionary)
  4. self.embed_tokens = embed_tokens
  5. assert args.decoder_layers == args.encoder_layers, f"""seq2seq rnn requires that encoder
  6. and decoder have same layers of rnn. got: {args.encoder_layers, args.decoder_layers}"""
  7. assert args.decoder_ffn_embed_dim == args.encoder_ffn_embed_dim*2, f"""seq2seq-rnn requires
  8. that decoder hidden to be 2*encoder hidden dim. got: {args.decoder_ffn_embed_dim, args.encoder_ffn_embed_dim*2}"""
  9. self.embed_dim = args.decoder_embed_dim
  10. self.hidden_dim = args.decoder_ffn_embed_dim
  11. self.num_layers = args.decoder_layers
  12. self.dropout_in_module = nn.Dropout(args.dropout)
  13. self.rnn = nn.GRU(
  14. self.embed_dim,
  15. self.hidden_dim,
  16. self.num_layers,
  17. dropout=args.dropout,
  18. batch_first=False,
  19. bidirectional=False
  20. )
  21. self.attention = AttentionLayer(
  22. self.embed_dim, self.hidden_dim, self.embed_dim, bias=False
  23. )
  24. # self.attention = None
  25. self.dropout_out_module = nn.Dropout(args.dropout)
  26. if self.hidden_dim != self.embed_dim:
  27. self.project_out_dim = nn.Linear(self.hidden_dim, self.embed_dim)
  28. else:
  29. self.project_out_dim = None
  30. #8000
  31. #print(self.embed_tokens.weight.shape[0])
  32. #256
  33. #print(self.embed_tokens.weight.shape[1])
  34. if args.share_decoder_input_output_embed:
  35. self.output_projection = nn.Linear(
  36. self.embed_tokens.weight.shape[1],
  37. self.embed_tokens.weight.shape[0],
  38. bias=False,
  39. )
  40. self.output_projection.weight = self.embed_tokens.weight
  41. else:
  42. self.output_projection = nn.Linear(
  43. self.output_embed_dim, len(dictionary), bias=False
  44. )
  45. nn.init.normal_(
  46. self.output_projection.weight, mean=0, std=self.output_embed_dim ** -0.5
  47. )
  48. def forward(self, prev_output_tokens, encoder_out, incremental_state=None, **unused):
  49. # extract the outputs from encoder
  50. encoder_outputs, encoder_hiddens, encoder_padding_mask = encoder_out
  51. # outputs: seq_len x batch x num_directions*hidden
  52. # encoder_hiddens: num_layers x batch x num_directions*encoder_hidden
  53. # padding_mask: seq_len x batch
  54. #if incremental_state is None:
  55. # print('incremental_state is None')
  56. #else:
  57. # print(incremental_state)
  58. if incremental_state is not None and len(incremental_state) > 0:
  59. # if the information from last timestep is retained, we can continue from there instead of starting from bos
  60. #print(prev_output_tokens.size())
  61. prev_output_tokens = prev_output_tokens[:, -1:]
  62. #print(prev_output_tokens.size())
  63. cache_state = self.get_incremental_state(incremental_state, "cached_state")
  64. prev_hiddens = cache_state["prev_hiddens"]
  65. else:
  66. # incremental state does not exist, either this is training time, or the first timestep of test time
  67. # prepare for seq2seq: pass the encoder_hidden to the decoder hidden states
  68. # print('incremental_state is None')
  69. prev_hiddens = encoder_hiddens
  70. #print(prev_output_tokens)
  71. bsz, seqlen = prev_output_tokens.size()
  72. # embed tokens
  73. #print("self.embed_tokens.weight",self.embed_tokens.weight.size())
  74. x = self.embed_tokens(prev_output_tokens)
  75. x = self.dropout_in_module(x)
  76. # B x T x C -> T x B x C B-batchsize T-sequencelength(timestep) C-embedfeatures
  77. x = x.transpose(0, 1)
  78. # decoder-to-encoder attention
  79. if self.attention is not None:
  80. x, attn = self.attention(x, encoder_outputs, encoder_padding_mask)
  81. # pass thru unidirectional RNN
  82. x, final_hiddens = self.rnn(x, prev_hiddens)
  83. # outputs = [sequence len, batch size, hid dim]
  84. # hidden = [num_layers * directions, batch size , hid dim]
  85. x = self.dropout_out_module(x)
  86. # project to embedding size (if hidden differs from embed size, and share_embedding is True,
  87. # we need to do an extra projection)
  88. if self.project_out_dim != None:
  89. x = self.project_out_dim(x)
  90. # project to vocab size
  91. x = self.output_projection(x)
  92. # T x B x C -> B x T x C
  93. x = x.transpose(1, 0)
  94. # if incremental, record the hidden states of current timestep, which will be restored in the next timestep
  95. cache_state = {
  96. "prev_hiddens": final_hiddens,
  97. }
  98. self.set_incremental_state(incremental_state, "cached_state", cache_state)
  99. return x, None
  100. def reorder_incremental_state(
  101. self,
  102. incremental_state,
  103. new_order,
  104. ):
  105. # This is used by fairseq's beam search. How and why is not particularly important here.
  106. cache_state = self.get_incremental_state(incremental_state, "cached_state")
  107. prev_hiddens = cache_state["prev_hiddens"]
  108. prev_hiddens = [p.index_select(0, new_order) for p in prev_hiddens]
  109. cache_state = {
  110. "prev_hiddens": torch.stack(prev_hiddens),
  111. }
  112. self.set_incremental_state(incremental_state, "cached_state", cache_state)
  113. return

 后面的seq2seq就是将encoder和decoder组合在一起

后面的一些都比较简单就直接略过,接下来是标签平滑交叉熵和NoamOpt,前者是在计算时考虑总体的标签,而不是单纯地让为0的prediction得到的结果为零,考虑了全体prediction,而NoamOpt就是在刚开始的时候快速提高lambda学习率,快速收敛,然后再退火,动态缩小学习率,使得准确收敛,后面就是做valid,和save,predict这些,都较为简单,其中有一个我自学了一下的,是scaler,这个东西是能够动态缩放梯度的,能够使得梯度计算更快捷,可以防止梯度爆炸,和梯度消失,做梯度剪枝,然后在做step更新模型参数时,会用unscale来重置缩放

  1. with autocast():
  2. #pprint.pprint(sample["net_input"])
  3. net_output = model.forward(**sample["net_input"])
  4. #print(net_output[0].size())
  5. #print(net_output[0])
  6. lprobs = F.log_softmax(net_output[0], -1)
  7. #print("lprobs",lprobs.size())
  8. #print("target",target.size())
  9. loss = criterion(lprobs.view(-1, lprobs.size(-1)), target.view(-1))
  10. # logging
  11. # Here,the loss is the tensor type,so use the item() func to extract the loss num.
  12. accum_loss += loss.item()
  13. # back-prop
  14. scaler.scale(loss).backward()
  15. scaler.unscale_(optimizer)
  16. optimizer.multiply_grads(1 / (sample_size or 1.0)) # (sample_size or 1.0) handles the case of a zero gradient
  17. gnorm = nn.utils.clip_grad_norm_(model.parameters(), config.clip_norm) # grad norm clipping prevents gradient exploding

补充,做strong_line的时候,只需要把RNN的encoder和decoder注释掉,然后加上TransformerEncoder和TransformerDecoder即可,然后更改一下config,把savedir里的文件名改为transformer。

然后要注意一下,你做这个作业的时候可能会出现和我类似的问题,就是可能最后test的prediction很离谱,这个时候可能是spm_model做字典预测的时候出问题了,我好像是自己不小心把test的文件名改了,把已经被分词化的文件改为了未做分词化的test文件,这个时候你查一下binarize做preprocess的log就能发现,假如说发现在对test做binarize的时候出现很多的unk,那就可能是这个问题,然后,本次作业不是有个做back_translation吗,这个的话,就是为了增大en的数据集。

结语:本次作业是我第一次深切的感受到nlp,上一个作业就做的比较快,主要是调参,对conformer也只是看了论文,没有更深层的感受,但是这次作业的预处理,对embedding,rnn等的学习,我觉得对能力提升帮助十分大

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/361218
推荐阅读
相关标签
  

闽ICP备14008679号