init
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
[UniLM: Unified Language Model Pre-training]
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
MIT License
|
||||
Copyright (c) Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -14,7 +12,7 @@ furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
NOTICES AND INFORMATION
|
||||
|
||||
Do Not Translate or Localize
|
||||
|
||||
This software incorporates material from third parties. Microsoft makes certain
|
||||
open source code available at http://3rdpartysource.microsoft.com, or you may
|
||||
send a check or money order for US $5.00, including the product name, the open
|
||||
source component name, and version number, to:
|
||||
|
||||
Source Code Compliance Team
|
||||
Microsoft Corporation
|
||||
One Microsoft Way
|
||||
Redmond, WA 98052
|
||||
USA
|
||||
|
||||
Notwithstanding any other terms, you may reverse engineer this software to the
|
||||
extent required to debug changes to any libraries licensed under the GNU Lesser
|
||||
General Public License.
|
||||
|
||||
===============================================================================
|
||||
|
||||
Component.
|
||||
|
||||
huggingface/transformers
|
||||
|
||||
Open Source License/Copyright Notice.
|
||||
|
||||
```
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
@@ -1,19 +1,316 @@
|
||||
# UniLM
|
||||
**Unified Language Model Pre-training for Natural Language Understanding and Generation**
|
||||
|
||||
**\*\*\*\*\* New September 22nd, 2019: UniLM v1 release \*\*\*\*\***
|
||||
**\*\*\*\*\* New October 1st, 2019: UniLM v1 release \*\*\*\*\***
|
||||
|
||||
**UniLM v1**: the repo hosts the code and pre-trained models for the NeurIPS 2019 paper entitled "[Unified Language Model Pre-training for Natural Language Understanding and Generation](https://arxiv.org/abs/1905.03197)".
|
||||
|
||||
**UniLM v2**: the new pre-training protocol and implementation scheme (coming soon).
|
||||
|
||||
## Reference
|
||||
## Environment
|
||||
|
||||
If you find UniLM useful in your work, you can cite the paper as below:
|
||||
|
||||
@incollection{unilm_neurips2019,
|
||||
title = {Unified Language Model Pre-training for Natural Language Understanding and Generation},
|
||||
author = {Dong, Li and Yang, Nan and Wang, Wenhui and Wei, Furu and Liu, Xiaodong and Wang, Yu and Gao, Jianfeng and Zhou, Ming and Hon, Hsiao-Wuen},
|
||||
booktitle = {Advances in Neural Information Processing Systems 32},
|
||||
year = {2019},
|
||||
url = {https://arxiv.org/pdf/1905.03197.pdf}
|
||||
The recommended way to run the code is using docker:
|
||||
```bash
|
||||
alias=`whoami | cut -d'.' -f2`; docker run -it --rm --runtime=nvidia --ipc=host --privileged -v /home/${alias}:/home/${alias} pytorch/pytorch:1.1.0-cuda10.0-cudnn7.5-devel bash
|
||||
```
|
||||
|
||||
The docker is initialized by:
|
||||
```bash
|
||||
. .bashrc
|
||||
apt-get update
|
||||
apt-get install -y vim wget ssh
|
||||
|
||||
PWD_DIR=$(pwd)
|
||||
cd $(mktemp -d)
|
||||
git clone -q https://github.com/NVIDIA/apex.git
|
||||
cd apex
|
||||
git reset --hard 1603407bf49c7fc3da74fceb6a6c7b47fece2ef8
|
||||
python setup.py install --user --cuda_ext --cpp_ext
|
||||
cd $PWD_DIR
|
||||
|
||||
pip install --user tensorboardX six numpy tqdm path.py pandas scikit-learn lmdb pyarrow py-lz4framed methodtools
|
||||
```
|
||||
The mixed-precision training code requires the specific version of [NVIDIA/apex](https://github.com/NVIDIA/apex/tree/1603407bf49c7fc3da74fceb6a6c7b47fece2ef8), which only supports pytorch<1.2.0.
|
||||
|
||||
Install the repo as a package in the docker:
|
||||
```bash
|
||||
mkdir ~/code; cd ~/code
|
||||
git clone https://github.com/microsoft/unilm.git
|
||||
cd ~/code/unilm/src
|
||||
pip install --user --editable .
|
||||
```
|
||||
|
||||
## Pre-trained Models
|
||||
We release a large **cased** UniLM model pre-trained with **Wikipedia and BookCorpus** corpora. The model is trained by using the same model configuration and WordPiece vocabulary as BERT. The model parameters can be loaded as in the fine-tuning code.
|
||||
|
||||
The links to the pre-trained models:
|
||||
- [UniLMv1-large-cased](https://drive.google.com/open?id=1Zj_nZWO7YffaOInj3Q4SZyn09Mb3In-e): 24-layer, 1024-hidden, 16-heads, 340M parameters
|
||||
|
||||
## Fine-tuning
|
||||
We provide instructions on how to fine-tune UniLM as a sequence-to-sequence model to support various downstream natural language generation tasks as follows. It is recommended to use 2 or 4 v100-32G GPU cards to fine-tune the model. Gradient accumulation (`--gradient_accumulation_steps`) can be enabled if there is an OOM error.
|
||||
|
||||
### Abstractive Summarization - [Gigaword](https://github.com/harvardnlp/sent-summary) (10K)
|
||||
|
||||
In the example, only 10K examples of the Gigaword training data are used to fine-tune UniLM. As shown in the following table, pre-training significantly improves performance for low-resource settings.
|
||||
|
||||
| Model | ROUGE-1 | ROUGE-2 | ROUGE-L |
|
||||
| ------------------------------------------------------------------- | --------- | --------- | --------- |
|
||||
| [Transformer](http://proceedings.mlr.press/v97/song19d/song19d.pdf) | 10.97 | 2.23 | 10.42 |
|
||||
| **UniLM** | **34.21** | **15.28** | **31.54** |
|
||||
|
||||
The data can be downloaded from [here](https://drive.google.com/open?id=1USoQ8lJgN8kAWnUnRrupMGrPMLlDVqlV).
|
||||
|
||||
```bash
|
||||
# run fine-tuning
|
||||
DATA_DIR=/{path_of_data}/gigaword
|
||||
OUTPUT_DIR=/{path_of_fine-tuned_model}/
|
||||
MODEL_RECOVER_PATH=/{path_of_pre-trained_model}/unilmv1-large-cased.bin
|
||||
export PYTORCH_PRETRAINED_BERT_CACHE=/{tmp_folder}/bert-cased-pretrained-cache
|
||||
export CUDA_VISIBLE_DEVICES=0,1,2,3
|
||||
python biunilm/run_seq2seq.py --do_train --fp16 --amp --num_workers 0 \
|
||||
--bert_model bert-large-cased --new_segment_ids --tokenized_input \
|
||||
--data_dir ${DATA_DIR} --src_file train.src.10k --tgt_file train.tgt.10k \
|
||||
--output_dir ${OUTPUT_DIR}/bert_save \
|
||||
--log_dir ${OUTPUT_DIR}/bert_log \
|
||||
--model_recover_path ${MODEL_RECOVER_PATH} \
|
||||
--max_seq_length 192 --max_position_embeddings 192 \
|
||||
--trunc_seg a --always_truncate_tail --max_len_b 64 \
|
||||
--mask_prob 0.7 --max_pred 64 \
|
||||
--train_batch_size 128 --gradient_accumulation_steps 1 \
|
||||
--learning_rate 0.00001 --warmup_proportion 0.1 --label_smoothing 0.1 \
|
||||
--num_train_epochs 30
|
||||
```
|
||||
|
||||
We provide a fine-tuned checkpoint (downloaded from [here](https://drive.google.com/open?id=1yKFBpT2dbN5d6WBjFlJqlXs9DQKCbRWe)) used for decoding. The inference and evaluation process is conducted as follows:
|
||||
```bash
|
||||
# run decoding
|
||||
DATA_DIR=/{path_of_data}/gigaword
|
||||
MODEL_RECOVER_PATH=/{path_of_fine-tuned_model}/ggw10k_model.bin
|
||||
EVAL_SPLIT=test
|
||||
export PYTORCH_PRETRAINED_BERT_CACHE=/{tmp_folder}/bert-cased-pretrained-cache
|
||||
# run decoding
|
||||
python biunilm/decode_seq2seq.py --fp16 --amp --bert_model bert-large-cased --new_segment_ids --mode s2s --need_score_traces \
|
||||
--input_file ${DATA_DIR}/${EVAL_SPLIT}.src --split ${EVAL_SPLIT} --tokenized_input \
|
||||
--model_recover_path ${MODEL_RECOVER_PATH} \
|
||||
--max_seq_length 192 --max_tgt_length 32 \
|
||||
--batch_size 64 --beam_size 5 --length_penalty 0 \
|
||||
--forbid_duplicate_ngrams --forbid_ignore_word "."
|
||||
# run evaluation
|
||||
python gigaword/eval.py --pred ${MODEL_RECOVER_PATH}.${EVAL_SPLIT} \
|
||||
--gold ${DATA_DIR}/org_data/${EVAL_SPLIT}.tgt.txt --perl
|
||||
```
|
||||
|
||||
The program `eval.py` generates a post-processed output file `${MODEL_RECOVER_PATH}.${EVAL_SPLIT}.post` (downloaded from [here](https://drive.google.com/open?id=15R7IvOVT3irdH3d2eqHPs_5Lbh1LIy8U)).
|
||||
|
||||
### Abstractive Summarization - [Gigaword](https://github.com/harvardnlp/sent-summary)
|
||||
|
||||
The training set of Gigaword contains 3.8M examples for headline generation.
|
||||
|
||||
| Model | ROUGE-1 | ROUGE-2 | ROUGE-L |
|
||||
| -------------------------------------------------------------------------------- | --------- | --------- | --------- |
|
||||
| [OpenNMT](https://aclweb.org/anthology/P18-1015) | 36.73 | 17.86 | 33.68 |
|
||||
| [Re3Sum (Cao et al., 2018)](https://aclweb.org/anthology/P18-1015) | 37.04 | 19.03 | 34.46 |
|
||||
| [MASS (Song et al., 2019)](http://proceedings.mlr.press/v97/song19d/song19d.pdf) | 38.73 | 19.71 | 35.96 |
|
||||
| [BertShare (Rothe et al., 2019)](https://arxiv.org/pdf/1907.12461.pdf) | 38.13 | 19.81 | 35.62 |
|
||||
| **UniLM** | **38.90** | **20.05** | **36.00** |
|
||||
|
||||
The data can be downloaded from [here](https://drive.google.com/open?id=1USoQ8lJgN8kAWnUnRrupMGrPMLlDVqlV).
|
||||
|
||||
```bash
|
||||
# run fine-tuning
|
||||
DATA_DIR=/{path_of_data}/gigaword
|
||||
OUTPUT_DIR=/{path_of_fine-tuned_model}/
|
||||
MODEL_RECOVER_PATH=/{path_of_pre-trained_model}/unilmv1-large-cased.bin
|
||||
export PYTORCH_PRETRAINED_BERT_CACHE=/{tmp_folder}/bert-cased-pretrained-cache
|
||||
export CUDA_VISIBLE_DEVICES=0,1,2,3
|
||||
python biunilm/run_seq2seq.py --do_train --fp16 --amp --num_workers 0 \
|
||||
--bert_model bert-large-cased --new_segment_ids --tokenized_input \
|
||||
--data_dir ${DATA_DIR} \
|
||||
--output_dir ${OUTPUT_DIR}/bert_save \
|
||||
--log_dir ${OUTPUT_DIR}/bert_log \
|
||||
--model_recover_path ${MODEL_RECOVER_PATH} \
|
||||
--max_seq_length 192 --max_position_embeddings 192 \
|
||||
--trunc_seg a --always_truncate_tail --max_len_a 0 --max_len_b 64 \
|
||||
--mask_prob 0.7 --max_pred 48 \
|
||||
--train_batch_size 128 --gradient_accumulation_steps 1 \
|
||||
--learning_rate 0.00003 --warmup_proportion 0.1 --label_smoothing 0.1 \
|
||||
--num_train_epochs 30
|
||||
```
|
||||
|
||||
The size of full training data (3.8M) is quite large. We can stop the fine-tuning procedure after 10 epochs.
|
||||
|
||||
We provide a fine-tuned checkpoint (downloaded from [here](https://drive.google.com/open?id=1jOI2nO16Uz4a0OWZ7Ro-jnD54MHMDlsv)) used for decoding. The inference and evaluation process is conducted as follows:
|
||||
```bash
|
||||
DATA_DIR=/{path_of_data}/gigaword
|
||||
MODEL_RECOVER_PATH=/{path_of_fine-tuned_model}/ggw38m_model.bin
|
||||
EVAL_SPLIT=test
|
||||
export PYTORCH_PRETRAINED_BERT_CACHE=/{tmp_folder}/bert-cased-pretrained-cache
|
||||
# run decoding
|
||||
python biunilm/decode_seq2seq.py --fp16 --amp --bert_model bert-large-cased --new_segment_ids --mode s2s --need_score_traces \
|
||||
--input_file ${DATA_DIR}/${EVAL_SPLIT}.src --split ${EVAL_SPLIT} --tokenized_input \
|
||||
--model_recover_path ${MODEL_RECOVER_PATH} \
|
||||
--max_seq_length 192 --max_tgt_length 32 \
|
||||
--batch_size 64 --beam_size 5 --length_penalty 0 \
|
||||
--forbid_duplicate_ngrams --forbid_ignore_word "."
|
||||
# apply length penalty
|
||||
python biunilm/gen_seq_from_trace.py --bert_model bert-large-cased --alpha 0.6 \
|
||||
--input ${MODEL_RECOVER_PATH}.${EVAL_SPLIT}
|
||||
# run evaluation
|
||||
python gigaword/eval.py --pred ${MODEL_RECOVER_PATH}.${EVAL_SPLIT}.alp0.6 \
|
||||
--gold ${DATA_DIR}/org_data/${EVAL_SPLIT}.tgt.txt --perl
|
||||
```
|
||||
|
||||
The program `eval.py` generates a post-processed output file `${MODEL_RECOVER_PATH}.${EVAL_SPLIT}.alp0.6.post` (downloaded from [here](https://drive.google.com/open?id=1oycvzMC6ZoWZV7BOt5OlZ7q0SxM_0Zc9)).
|
||||
|
||||
### Abstractive Summarization - [CNN / Daily Mail](https://github.com/harvardnlp/sent-summary)
|
||||
|
||||
| Model | ROUGE-1 | ROUGE-2 | ROUGE-L |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------- | --------- |
|
||||
| [PGNet (See et al., 2017)](https://www.aclweb.org/anthology/P17-1099) | 39.53 | 17.28 | 36.38 |
|
||||
| [Bottom-Up (Gehrmann et al., 2018)](https://www.aclweb.org/anthology/D18-1443) | 41.22 | 18.68 | 38.34 |
|
||||
| [GPT-2 TL;DR: (Radford et al., 2019)](https://d4mucfpksywv.cloudfront.net/better-language-models/language_models_are_unsupervised_multitask_learners.pdf) | 29.34 | 8.27 | 26.58 |
|
||||
| [MASS (Song et al., 2019)](https://github.com/microsoft/MASS#results-on-abstractive-summarization-9272019) | 42.12 | 19.50 | 39.01 |
|
||||
| [BertShare (Rothe et al., 2019)](https://arxiv.org/pdf/1907.12461.pdf) | 39.25 | 18.09 | 36.45 |
|
||||
| [BertSumAbs (Liu and Lapata, 2019)](https://arxiv.org/pdf/1908.08345.pdf) | 41.72 | 19.39 | 38.76 |
|
||||
| **UniLM** | **43.08** | **20.43** | **40.34** |
|
||||
|
||||
The data can be downloaded from [here](https://drive.google.com/open?id=1jiDbDbAsqy_5BM79SmX6aSu5DQVCAZq1).
|
||||
|
||||
```bash
|
||||
# run fine-tuning
|
||||
DATA_DIR=/{path_of_data}/cnn_dailymail
|
||||
OUTPUT_DIR=/{path_of_fine-tuned_model}/
|
||||
MODEL_RECOVER_PATH=/{path_of_pre-trained_model}/unilmv1-large-cased.bin
|
||||
export PYTORCH_PRETRAINED_BERT_CACHE=/{tmp_folder}/bert-cased-pretrained-cache
|
||||
export CUDA_VISIBLE_DEVICES=0,1,2,3
|
||||
python biunilm/run_seq2seq.py --do_train --fp16 --amp --num_workers 0 \
|
||||
--bert_model bert-large-cased --new_segment_ids --tokenized_input \
|
||||
--data_dir ${DATA_DIR} \
|
||||
--output_dir ${OUTPUT_DIR}/bert_save \
|
||||
--log_dir ${OUTPUT_DIR}/bert_log \
|
||||
--model_recover_path ${MODEL_RECOVER_PATH} \
|
||||
--max_seq_length 768 --max_position_embeddings 768 \
|
||||
--trunc_seg a --always_truncate_tail \
|
||||
--max_len_a 568 --max_len_b 200 \
|
||||
--mask_prob 0.7 --max_pred 140 \
|
||||
--train_batch_size 48 --gradient_accumulation_steps 2 \
|
||||
--learning_rate 0.00003 --warmup_proportion 0.1 --label_smoothing 0.1 \
|
||||
--num_train_epochs 30
|
||||
```
|
||||
|
||||
We provide a fine-tuned checkpoint (downloaded from [here](https://drive.google.com/open?id=1RyJxShxC9tDYVAyZwUwqkSoQ3l5DfjuE)) used for decoding. The inference and evaluation process is conducted as follows:
|
||||
|
||||
```bash
|
||||
DATA_DIR=/{path_of_data}/cnn_dailymail
|
||||
MODEL_RECOVER_PATH=/{path_of_fine-tuned_model}/cnndm_model.bin
|
||||
EVAL_SPLIT=test
|
||||
export PYTORCH_PRETRAINED_BERT_CACHE=/{tmp_folder}/bert-cased-pretrained-cache
|
||||
# run decoding
|
||||
python biunilm/decode_seq2seq.py --fp16 --amp --bert_model bert-large-cased --new_segment_ids --mode s2s --need_score_traces \
|
||||
--input_file ${DATA_DIR}/${EVAL_SPLIT}.src --split ${EVAL_SPLIT} --tokenized_input \
|
||||
--model_recover_path ${MODEL_RECOVER_PATH} \
|
||||
--max_seq_length 768 --max_tgt_length 128 \
|
||||
--batch_size 64 --beam_size 5 --length_penalty 0 \
|
||||
--forbid_duplicate_ngrams --forbid_ignore_word ".|[X_SEP]"
|
||||
# apply length penalty
|
||||
python biunilm/gen_seq_from_trace.py --bert_model bert-large-cased --alpha 1.0 \
|
||||
--input ${MODEL_RECOVER_PATH}.${EVAL_SPLIT}
|
||||
# run evaluation
|
||||
python gigaword/eval.py --pred ${MODEL_RECOVER_PATH}.${EVAL_SPLIT}.alp1.0 \
|
||||
--gold ${DATA_DIR}/org_data/${EVAL_SPLIT}.summary --trunc_len 70 --perl
|
||||
```
|
||||
|
||||
The program `eval.py` generates a post-processed output file `${MODEL_RECOVER_PATH}.${EVAL_SPLIT}.alp1.0.post` (downloaded from [here](https://drive.google.com/open?id=1p93XD0wo3YvyxZnNYywujtnQoNCDiTF7)).
|
||||
|
||||
### Question Generation - [SQuAD](https://arxiv.org/abs/1806.03822)
|
||||
|
||||
We present the results following the same [data split](https://github.com/xinyadu/nqg/tree/master/data) and [evaluation scripts](https://github.com/xinyadu/nqg/tree/master/qgevalcap) as in [(Du et al., 2017)](https://arxiv.org/pdf/1705.00106.pdf).
|
||||
|
||||
| Model | BLEU-4 | METEOR | ROUGE-L |
|
||||
| ------------------------------------------------------------------ | --------- | --------- | --------- |
|
||||
| [(Du and Cardie, 2018)](https://www.aclweb.org/anthology/P18-1177) | 15.16 | 19.12 | - |
|
||||
| [(Zhang and Bansal, 2019)](https://arxiv.org/pdf/1909.06356.pdf) | 18.37 | 22.65 | 46.68 |
|
||||
| **UniLM** | **22.78** | **25.49** | **51.57** |
|
||||
|
||||
We also report the results following the data split as in [(Zhao et al., 2018)](https://aclweb.org/anthology/D18-1424), which uses the reversed dev-test setup.
|
||||
|
||||
| Model | BLEU-4 | METEOR | ROUGE-L |
|
||||
| ---------------------------------------------------------------- | --------- | --------- | --------- |
|
||||
| [(Zhao et al., 2018)](https://aclweb.org/anthology/D18-1424) | 16.38 | 20.25 | 44.48 |
|
||||
| [(Zhang and Bansal, 2019)](https://arxiv.org/pdf/1909.06356.pdf) | 20.76 | 24.20 | 48.91 |
|
||||
| **UniLM** | **24.32** | **26.10** | **52.69** |
|
||||
|
||||
Note: If we directly use the tokenized references provided by [Du et al. (2017)](https://arxiv.org/pdf/1705.00106.pdf), the results are (22.17 BLEU-4 / 25.47 METEOR / 51.53 ROUGE-L) on the [raw data split](https://github.com/xinyadu/nqg/tree/master/data), and (23.69 BLEU-4 / 26.08 METEOR / 52.70 ROUGE-L) in the reversed dev-test setup.
|
||||
|
||||
Our processed data can be downloaded from [here](https://drive.google.com/open?id=11E3Ij-ctbRUTIQjueresZpoVzLMPlVUZ).
|
||||
|
||||
```bash
|
||||
# run fine-tuning
|
||||
DATA_DIR=/{path_of_data}/qg/train
|
||||
OUTPUT_DIR=/{path_of_fine-tuned_model}/
|
||||
MODEL_RECOVER_PATH=/{path_of_pre-trained_model}/unilmv1-large-cased.bin
|
||||
export PYTORCH_PRETRAINED_BERT_CACHE=/{tmp_folder}/bert-cased-pretrained-cache
|
||||
export CUDA_VISIBLE_DEVICES=0,1,2,3
|
||||
python biunilm/run_seq2seq.py --do_train --num_workers 0 \
|
||||
--bert_model bert-large-cased --new_segment_ids --tokenized_input \
|
||||
--data_dir ${DATA_DIR} --src_file train.pa.tok.txt --tgt_file train.q.tok.txt \
|
||||
--output_dir ${OUTPUT_DIR}/bert_save \
|
||||
--log_dir ${OUTPUT_DIR}/bert_log \
|
||||
--model_recover_path ${MODEL_RECOVER_PATH} \
|
||||
--max_seq_length 512 --max_position_embeddings 512 \
|
||||
--mask_prob 0.7 --max_pred 48 \
|
||||
--train_batch_size 32 --gradient_accumulation_steps 2 \
|
||||
--learning_rate 0.00002 --warmup_proportion 0.1 --label_smoothing 0.1 \
|
||||
--num_train_epochs 10
|
||||
```
|
||||
|
||||
We provide a fine-tuned checkpoint (downloaded from [here](https://drive.google.com/open?id=1JN2wnkSRotwUnJ_Z-AbWwoPdP53Gcfsn)) used for decoding. The inference and evaluation process is conducted as follows:
|
||||
|
||||
```bash
|
||||
DATA_DIR=/{path_of_data}/qg/test
|
||||
MODEL_RECOVER_PATH=/{path_of_fine-tuned_model}/qg_model.bin
|
||||
EVAL_SPLIT=test
|
||||
export PYTORCH_PRETRAINED_BERT_CACHE=/{tmp_folder}/bert-cased-pretrained-cache
|
||||
# run decoding
|
||||
python biunilm/decode_seq2seq.py --bert_model bert-large-cased --new_segment_ids --mode s2s \
|
||||
--input_file ${DATA_DIR}/$test.pa.tok.txt --split ${EVAL_SPLIT} --tokenized_input \
|
||||
--model_recover_path ${MODEL_RECOVER_PATH} \
|
||||
--max_seq_length 512 --max_tgt_length 48 \
|
||||
--batch_size 16 --beam_size 1 --length_penalty 0
|
||||
# run evaluation using our tokenized data as reference
|
||||
python qg/eval_on_unilm_tokenized_ref.py --out_file qg/output/qg.test.output.txt
|
||||
# run evaluation using tokenized data of Du et al. (2017) as reference
|
||||
python qg/eval.py --out_file qg/output/qg.test.output.txt
|
||||
```
|
||||
|
||||
The output files can be downloaded from [here](https://drive.google.com/open?id=1MdaRftgl_HMqN7DLvYmw-zKkvOBZCP6U).
|
||||
|
||||
Note: the evaluation code files `qg/eval_on_unilm_tokenized_ref.py` and `qg/eval.py` are in Python 2.*, because they are dependent on the [evaluation scripts](https://github.com/xinyadu/nqg/tree/master/qgevalcap) of [Du et al., (2017)](https://arxiv.org/pdf/1705.00106.pdf).
|
||||
|
||||
## Citation
|
||||
|
||||
If you find UniLM useful in your work, you can cite the following paper:
|
||||
```
|
||||
@inproceedings{unilm,
|
||||
title={Unified Language Model Pre-training for Natural Language Understanding and Generation},
|
||||
author={Dong, Li and Yang, Nan and Wang, Wenhui and Wei, Furu and Liu, Xiaodong and Wang, Yu and Gao, Jianfeng and Zhou, Ming and Hon, Hsiao-Wuen},
|
||||
year={2019},
|
||||
booktitle = "33rd Conference on Neural Information Processing Systems (NeurIPS 2019)"
|
||||
}
|
||||
```
|
||||
|
||||
## Acknowledgments
|
||||
Our code is based on [pytorch-transformers v0.4.0](https://github.com/huggingface/pytorch-transformers/tree/v0.4.0). We thank the authors for their wonderful open-source efforts.
|
||||
|
||||
## License
|
||||
This project is licensed under the license found in the LICENSE file in the root directory of this source tree.
|
||||
Portions of the source code are based on the [pytorch-transformers v0.4.0](https://github.com/huggingface/pytorch-transformers/tree/v0.4.0) project.
|
||||
|
||||
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct)
|
||||
|
||||
### Contact Information
|
||||
|
||||
For help or issues using UniLM, please submit a GitHub issue.
|
||||
|
||||
For personal communication related to UniLM, please contact Li Dong (`lidong1@microsoft.com`), Furu Wei (`fuwei@microsoft.com`).
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
"""BERT finetuning runner."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import logging
|
||||
import glob
|
||||
import argparse
|
||||
import math
|
||||
from tqdm import tqdm, trange
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.utils.data import DataLoader, RandomSampler
|
||||
from torch.utils.data.distributed import DistributedSampler
|
||||
import random
|
||||
import pickle
|
||||
|
||||
from pytorch_pretrained_bert.tokenization import BertTokenizer, WhitespaceTokenizer
|
||||
from pytorch_pretrained_bert.modeling import BertForSeq2SeqDecoder
|
||||
from pytorch_pretrained_bert.optimization import BertAdam, warmup_linear
|
||||
|
||||
from nn.data_parallel import DataParallelImbalance
|
||||
import biunilm.seq2seq_loader as seq2seq_loader
|
||||
|
||||
|
||||
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
|
||||
datefmt='%m/%d/%Y %H:%M:%S',
|
||||
level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def detokenize(tk_list):
|
||||
r_list = []
|
||||
for tk in tk_list:
|
||||
if tk.startswith('##') and len(r_list) > 0:
|
||||
r_list[-1] = r_list[-1] + tk[2:]
|
||||
else:
|
||||
r_list.append(tk)
|
||||
return r_list
|
||||
|
||||
|
||||
def ascii_print(text):
|
||||
text = text.encode("ascii", "ignore")
|
||||
print(text)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# Required parameters
|
||||
parser.add_argument("--bert_model", default=None, type=str, required=True,
|
||||
help="Bert pre-trained model selected in the list: bert-base-uncased, "
|
||||
"bert-large-uncased, bert-base-cased, bert-base-multilingual, bert-base-chinese.")
|
||||
parser.add_argument("--model_recover_path", default=None, type=str,
|
||||
help="The file of fine-tuned pretraining model.")
|
||||
parser.add_argument("--max_seq_length", default=512, type=int,
|
||||
help="The maximum total input sequence length after WordPiece tokenization. \n"
|
||||
"Sequences longer than this will be truncated, and sequences shorter \n"
|
||||
"than this will be padded.")
|
||||
parser.add_argument('--ffn_type', default=0, type=int,
|
||||
help="0: default mlp; 1: W((Wx+b) elem_prod x);")
|
||||
parser.add_argument('--num_qkv', default=0, type=int,
|
||||
help="Number of different <Q,K,V>.")
|
||||
parser.add_argument('--seg_emb', action='store_true',
|
||||
help="Using segment embedding for self-attention.")
|
||||
|
||||
# decoding parameters
|
||||
parser.add_argument('--fp16', action='store_true',
|
||||
help="Whether to use 16-bit float precision instead of 32-bit")
|
||||
parser.add_argument('--amp', action='store_true',
|
||||
help="Whether to use amp for fp16")
|
||||
parser.add_argument("--input_file", type=str, help="Input file")
|
||||
parser.add_argument('--subset', type=int, default=0,
|
||||
help="Decode a subset of the input dataset.")
|
||||
parser.add_argument("--output_file", type=str, help="output file")
|
||||
parser.add_argument("--split", type=str, default="",
|
||||
help="Data split (train/val/test).")
|
||||
parser.add_argument('--tokenized_input', action='store_true',
|
||||
help="Whether the input is tokenized.")
|
||||
parser.add_argument('--seed', type=int, default=123,
|
||||
help="random seed for initialization")
|
||||
parser.add_argument("--do_lower_case", action='store_true',
|
||||
help="Set this flag if you are using an uncased model.")
|
||||
parser.add_argument('--new_segment_ids', action='store_true',
|
||||
help="Use new segment ids for bi-uni-directional LM.")
|
||||
parser.add_argument('--new_pos_ids', action='store_true',
|
||||
help="Use new position ids for LMs.")
|
||||
parser.add_argument('--batch_size', type=int, default=4,
|
||||
help="Batch size for decoding.")
|
||||
parser.add_argument('--beam_size', type=int, default=1,
|
||||
help="Beam size for searching")
|
||||
parser.add_argument('--length_penalty', type=float, default=0,
|
||||
help="Length penalty for beam search")
|
||||
|
||||
parser.add_argument('--forbid_duplicate_ngrams', action='store_true')
|
||||
parser.add_argument('--forbid_ignore_word', type=str, default=None,
|
||||
help="Forbid the word during forbid_duplicate_ngrams")
|
||||
parser.add_argument("--min_len", default=None, type=int)
|
||||
parser.add_argument('--need_score_traces', action='store_true')
|
||||
parser.add_argument('--ngram_size', type=int, default=3)
|
||||
parser.add_argument('--mode', default="s2s",
|
||||
choices=["s2s", "l2r", "both"])
|
||||
parser.add_argument('--max_tgt_length', type=int, default=128,
|
||||
help="maximum length of target sequence")
|
||||
parser.add_argument('--s2s_special_token', action='store_true',
|
||||
help="New special tokens ([S2S_SEP]/[S2S_CLS]) of S2S.")
|
||||
parser.add_argument('--s2s_add_segment', action='store_true',
|
||||
help="Additional segmental for the encoder of S2S.")
|
||||
parser.add_argument('--s2s_share_segment', action='store_true',
|
||||
help="Sharing segment embeddings for the encoder of S2S (used with --s2s_add_segment).")
|
||||
parser.add_argument('--pos_shift', action='store_true',
|
||||
help="Using position shift for fine-tuning.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.need_score_traces and args.beam_size <= 1:
|
||||
raise ValueError(
|
||||
"Score trace is only available for beam search with beam size > 1.")
|
||||
if args.max_tgt_length >= args.max_seq_length - 2:
|
||||
raise ValueError("Maximum tgt length exceeds max seq length - 2.")
|
||||
|
||||
device = torch.device(
|
||||
"cuda" if torch.cuda.is_available() else "cpu")
|
||||
n_gpu = torch.cuda.device_count()
|
||||
|
||||
random.seed(args.seed)
|
||||
np.random.seed(args.seed)
|
||||
torch.manual_seed(args.seed)
|
||||
if n_gpu > 0:
|
||||
torch.cuda.manual_seed_all(args.seed)
|
||||
|
||||
tokenizer = BertTokenizer.from_pretrained(
|
||||
args.bert_model, do_lower_case=args.do_lower_case)
|
||||
|
||||
tokenizer.max_len = args.max_seq_length
|
||||
|
||||
pair_num_relation = 0
|
||||
bi_uni_pipeline = []
|
||||
bi_uni_pipeline.append(seq2seq_loader.Preprocess4Seq2seqDecoder(list(tokenizer.vocab.keys()), tokenizer.convert_tokens_to_ids, args.max_seq_length, max_tgt_length=args.max_tgt_length, new_segment_ids=args.new_segment_ids,
|
||||
mode="s2s", num_qkv=args.num_qkv, s2s_special_token=args.s2s_special_token, s2s_add_segment=args.s2s_add_segment, s2s_share_segment=args.s2s_share_segment, pos_shift=args.pos_shift))
|
||||
|
||||
amp_handle = None
|
||||
if args.fp16 and args.amp:
|
||||
from apex import amp
|
||||
amp_handle = amp.init(enable_caching=True)
|
||||
logger.info("enable fp16 with amp")
|
||||
|
||||
# Prepare model
|
||||
cls_num_labels = 2
|
||||
type_vocab_size = 6 + \
|
||||
(1 if args.s2s_add_segment else 0) if args.new_segment_ids else 2
|
||||
mask_word_id, eos_word_ids, sos_word_id = tokenizer.convert_tokens_to_ids(
|
||||
["[MASK]", "[SEP]", "[S2S_SOS]"])
|
||||
forbid_ignore_set = None
|
||||
if args.forbid_ignore_word:
|
||||
w_list = []
|
||||
for w in args.forbid_ignore_word.split('|'):
|
||||
if w.startswith('[') and w.endswith(']'):
|
||||
w_list.append(w.upper())
|
||||
else:
|
||||
w_list.append(w)
|
||||
forbid_ignore_set = set(tokenizer.convert_tokens_to_ids(w_list))
|
||||
print(args.model_recover_path)
|
||||
for model_recover_path in glob.glob(args.model_recover_path.strip()):
|
||||
logger.info("***** Recover model: %s *****", model_recover_path)
|
||||
model_recover = torch.load(model_recover_path)
|
||||
model = BertForSeq2SeqDecoder.from_pretrained(args.bert_model, state_dict=model_recover, num_labels=cls_num_labels, num_rel=pair_num_relation, type_vocab_size=type_vocab_size, task_idx=3, mask_word_id=mask_word_id, search_beam_size=args.beam_size,
|
||||
length_penalty=args.length_penalty, eos_id=eos_word_ids, sos_id=sos_word_id, forbid_duplicate_ngrams=args.forbid_duplicate_ngrams, forbid_ignore_set=forbid_ignore_set, ngram_size=args.ngram_size, min_len=args.min_len, mode=args.mode, max_position_embeddings=args.max_seq_length, ffn_type=args.ffn_type, num_qkv=args.num_qkv, seg_emb=args.seg_emb, pos_shift=args.pos_shift)
|
||||
del model_recover
|
||||
|
||||
if args.fp16:
|
||||
model.half()
|
||||
model.to(device)
|
||||
if n_gpu > 1:
|
||||
model = torch.nn.DataParallel(model)
|
||||
|
||||
torch.cuda.empty_cache()
|
||||
model.eval()
|
||||
next_i = 0
|
||||
max_src_length = args.max_seq_length - 2 - args.max_tgt_length
|
||||
|
||||
with open(args.input_file, encoding="utf-8") as fin:
|
||||
input_lines = [x.strip() for x in fin.readlines()]
|
||||
if args.subset > 0:
|
||||
logger.info("Decoding subset: %d", args.subset)
|
||||
input_lines = input_lines[:args.subset]
|
||||
data_tokenizer = WhitespaceTokenizer() if args.tokenized_input else tokenizer
|
||||
input_lines = [data_tokenizer.tokenize(
|
||||
x)[:max_src_length] for x in input_lines]
|
||||
input_lines = sorted(list(enumerate(input_lines)),
|
||||
key=lambda x: -len(x[1]))
|
||||
output_lines = [""] * len(input_lines)
|
||||
score_trace_list = [None] * len(input_lines)
|
||||
total_batch = math.ceil(len(input_lines) / args.batch_size)
|
||||
|
||||
with tqdm(total=total_batch) as pbar:
|
||||
while next_i < len(input_lines):
|
||||
_chunk = input_lines[next_i:next_i + args.batch_size]
|
||||
buf_id = [x[0] for x in _chunk]
|
||||
buf = [x[1] for x in _chunk]
|
||||
next_i += args.batch_size
|
||||
max_a_len = max([len(x) for x in buf])
|
||||
instances = []
|
||||
for instance in [(x, max_a_len) for x in buf]:
|
||||
for proc in bi_uni_pipeline:
|
||||
instances.append(proc(instance))
|
||||
with torch.no_grad():
|
||||
batch = seq2seq_loader.batch_list_to_batch_tensors(
|
||||
instances)
|
||||
batch = [
|
||||
t.to(device) if t is not None else None for t in batch]
|
||||
input_ids, token_type_ids, position_ids, input_mask, mask_qkv, task_idx = batch
|
||||
traces = model(input_ids, token_type_ids,
|
||||
position_ids, input_mask, task_idx=task_idx, mask_qkv=mask_qkv)
|
||||
if args.beam_size > 1:
|
||||
traces = {k: v.tolist() for k, v in traces.items()}
|
||||
output_ids = traces['pred_seq']
|
||||
else:
|
||||
output_ids = traces.tolist()
|
||||
for i in range(len(buf)):
|
||||
w_ids = output_ids[i]
|
||||
output_buf = tokenizer.convert_ids_to_tokens(w_ids)
|
||||
output_tokens = []
|
||||
for t in output_buf:
|
||||
if t in ("[SEP]", "[PAD]"):
|
||||
break
|
||||
output_tokens.append(t)
|
||||
output_sequence = ' '.join(detokenize(output_tokens))
|
||||
output_lines[buf_id[i]] = output_sequence
|
||||
if args.need_score_traces:
|
||||
score_trace_list[buf_id[i]] = {
|
||||
'scores': traces['scores'][i], 'wids': traces['wids'][i], 'ptrs': traces['ptrs'][i]}
|
||||
pbar.update(1)
|
||||
if args.output_file:
|
||||
fn_out = args.output_file
|
||||
else:
|
||||
fn_out = model_recover_path+'.'+args.split
|
||||
with open(fn_out, "w", encoding="utf-8") as fout:
|
||||
for l in output_lines:
|
||||
fout.write(l)
|
||||
fout.write("\n")
|
||||
|
||||
if args.need_score_traces:
|
||||
with open(fn_out + ".trace.pickle", "wb") as fout_trace:
|
||||
pickle.dump(
|
||||
{"version": 0.0, "num_samples": len(input_lines)}, fout_trace)
|
||||
for x in score_trace_list:
|
||||
pickle.dump(x, fout_trace)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,145 @@
|
||||
import pickle
|
||||
import math
|
||||
import argparse
|
||||
import glob
|
||||
from pathlib import Path
|
||||
from tqdm import tqdm
|
||||
import unicodedata
|
||||
|
||||
from pytorch_pretrained_bert.tokenization import BertTokenizer
|
||||
|
||||
|
||||
def read_traces_from_file(file_name):
|
||||
with open(file_name, "rb") as fin:
|
||||
meta = pickle.load(fin)
|
||||
num_samples = meta["num_samples"]
|
||||
samples = []
|
||||
for _ in range(num_samples):
|
||||
samples.append(pickle.load(fin))
|
||||
return samples
|
||||
|
||||
|
||||
def get_best_sequence(sample, eos_id, pad_id, length_penalty=None, alpha=None, expect=None, min_len=None):
|
||||
# if not any((length_penalty, alpha, expect, min_len)):
|
||||
# raise ValueError(
|
||||
# "You can only specify length penalty or alpha, but not both.")
|
||||
scores = sample["scores"]
|
||||
wids_list = sample["wids"]
|
||||
ptrs = sample["ptrs"]
|
||||
|
||||
last_frame_id = len(scores) - 1
|
||||
for i, wids in enumerate(wids_list):
|
||||
if all(wid in (eos_id, pad_id) for wid in wids):
|
||||
last_frame_id = i
|
||||
break
|
||||
while all(wid == pad_id for wid in wids_list[last_frame_id]):
|
||||
last_frame_id -= 1
|
||||
|
||||
max_score = -math.inf
|
||||
frame_id = -1
|
||||
pos_in_frame = -1
|
||||
|
||||
for fid in range(last_frame_id + 1):
|
||||
for i, wid in enumerate(wids_list[fid]):
|
||||
if fid <= last_frame_id and scores[fid][i] >= 0:
|
||||
# skip paddings
|
||||
continue
|
||||
if (wid in (eos_id, pad_id)) or fid == last_frame_id:
|
||||
s = scores[fid][i]
|
||||
if length_penalty:
|
||||
if expect:
|
||||
s -= length_penalty * math.fabs(fid+1 - expect)
|
||||
else:
|
||||
s += length_penalty * (fid + 1)
|
||||
elif alpha:
|
||||
s = s / math.pow((5 + fid + 1) / 6.0, alpha)
|
||||
if s > max_score:
|
||||
# if (frame_id != -1) and min_len and (fid+1 < min_len):
|
||||
# continue
|
||||
max_score = s
|
||||
frame_id = fid
|
||||
pos_in_frame = i
|
||||
if frame_id == -1:
|
||||
seq = []
|
||||
else:
|
||||
seq = [wids_list[frame_id][pos_in_frame]]
|
||||
for fid in range(frame_id, 0, -1):
|
||||
pos_in_frame = ptrs[fid][pos_in_frame]
|
||||
seq.append(wids_list[fid - 1][pos_in_frame])
|
||||
seq.reverse()
|
||||
return seq
|
||||
|
||||
|
||||
def detokenize(tk_list):
|
||||
r_list = []
|
||||
for tk in tk_list:
|
||||
if tk.startswith('##') and len(r_list) > 0:
|
||||
r_list[-1] = r_list[-1] + tk[2:]
|
||||
else:
|
||||
r_list.append(tk)
|
||||
return r_list
|
||||
|
||||
|
||||
def simple_postprocess(tk_list):
|
||||
# truncate duplicate punctuations
|
||||
while tk_list and len(tk_list) > 4 and len(tk_list[-1]) == 1 and unicodedata.category(tk_list[-1]).startswith('P') and all(it == tk_list[-1] for it in tk_list[-4:]):
|
||||
tk_list = tk_list[:-3]
|
||||
return tk_list
|
||||
|
||||
|
||||
def main(args):
|
||||
tokenizer = BertTokenizer.from_pretrained(
|
||||
args.bert_model, do_lower_case=args.do_lower_case)
|
||||
|
||||
eos_id, pad_id = set(tokenizer.convert_tokens_to_ids(["[SEP]", "[PAD]"]))
|
||||
for input_file in tqdm(glob.glob(args.input)):
|
||||
if not Path(input_file+'.trace.pickle').exists():
|
||||
continue
|
||||
print(input_file)
|
||||
samples = read_traces_from_file(input_file+'.trace.pickle')
|
||||
|
||||
results = []
|
||||
|
||||
for s in samples:
|
||||
word_ids = get_best_sequence(s, eos_id, pad_id, alpha=args.alpha,
|
||||
length_penalty=args.length_penalty, expect=args.expect, min_len=args.min_len)
|
||||
tokens = tokenizer.convert_ids_to_tokens(word_ids)
|
||||
buf = []
|
||||
for t in tokens:
|
||||
if t in ("[SEP]", "[PAD]"):
|
||||
break
|
||||
else:
|
||||
buf.append(t)
|
||||
results.append(" ".join(simple_postprocess(detokenize(buf))))
|
||||
|
||||
fn_out = input_file+'.'
|
||||
if args.length_penalty:
|
||||
fn_out += 'lenp'+str(args.length_penalty)
|
||||
if args.expect:
|
||||
fn_out += 'exp'+str(args.expect)
|
||||
if args.alpha:
|
||||
fn_out += 'alp'+str(args.alpha)
|
||||
if args.min_len:
|
||||
fn_out += 'minl'+str(args.min_len)
|
||||
with open(fn_out, "w", encoding="utf-8") as fout:
|
||||
for line in results:
|
||||
fout.write(line)
|
||||
fout.write("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--input", type=str, help="Input file.")
|
||||
parser.add_argument("--bert_model", default=None, type=str, required=True,
|
||||
help="Bert pre-trained model selected in the list: bert-base-uncased, "
|
||||
"bert-large-uncased, bert-base-cased, bert-base-multilingual, bert-base-chinese.")
|
||||
parser.add_argument("--alpha", default=None, type=float)
|
||||
parser.add_argument("--length_penalty", default=None, type=float)
|
||||
parser.add_argument("--expect", default=None, type=float,
|
||||
help="Expectation of target length.")
|
||||
parser.add_argument("--min_len", default=None, type=int)
|
||||
parser.add_argument("--do_lower_case", action='store_true',
|
||||
help="Set this flag if you are using an uncased model.")
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args)
|
||||
@@ -0,0 +1,299 @@
|
||||
from random import randint, shuffle
|
||||
from random import random as rand
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
import torch.utils.data
|
||||
|
||||
|
||||
def get_random_word(vocab_words):
|
||||
i = randint(0, len(vocab_words)-1)
|
||||
return vocab_words[i]
|
||||
|
||||
|
||||
def batch_list_to_batch_tensors(batch):
|
||||
batch_tensors = []
|
||||
for x in zip(*batch):
|
||||
if x[0] is None:
|
||||
batch_tensors.append(None)
|
||||
elif isinstance(x[0], torch.Tensor):
|
||||
batch_tensors.append(torch.stack(x))
|
||||
else:
|
||||
batch_tensors.append(torch.tensor(x, dtype=torch.long))
|
||||
return batch_tensors
|
||||
|
||||
|
||||
class TrieNode(object):
|
||||
def __init__(self):
|
||||
self.children = {}
|
||||
self.is_leaf = False
|
||||
|
||||
def try_get_children(self, key):
|
||||
if key not in self.children:
|
||||
self.children[key] = TrieNode()
|
||||
return self.children[key]
|
||||
|
||||
|
||||
class TrieTree(object):
|
||||
def __init__(self):
|
||||
self.root = TrieNode()
|
||||
|
||||
def add(self, tokens):
|
||||
r = self.root
|
||||
for token in tokens:
|
||||
r = r.try_get_children(token)
|
||||
r.is_leaf = True
|
||||
|
||||
def get_pieces(self, tokens, offset):
|
||||
pieces = []
|
||||
r = self.root
|
||||
token_id = 0
|
||||
last_valid = 0
|
||||
match_count = 0
|
||||
while last_valid < len(tokens):
|
||||
if token_id < len(tokens) and tokens[token_id] in r.children:
|
||||
r = r.children[tokens[token_id]]
|
||||
match_count += 1
|
||||
if r.is_leaf:
|
||||
last_valid = token_id
|
||||
token_id += 1
|
||||
else:
|
||||
pieces.append(
|
||||
list(range(token_id - match_count + offset, last_valid + 1 + offset)))
|
||||
last_valid += 1
|
||||
token_id = last_valid
|
||||
r = self.root
|
||||
match_count = 0
|
||||
|
||||
return pieces
|
||||
|
||||
|
||||
def _get_word_split_index(tokens, st, end):
|
||||
split_idx = []
|
||||
i = st
|
||||
while i < end:
|
||||
if (not tokens[i].startswith('##')) or (i == st):
|
||||
split_idx.append(i)
|
||||
i += 1
|
||||
split_idx.append(end)
|
||||
return split_idx
|
||||
|
||||
|
||||
def _expand_whole_word(tokens, st, end):
|
||||
new_st, new_end = st, end
|
||||
while (new_st >= 0) and tokens[new_st].startswith('##'):
|
||||
new_st -= 1
|
||||
while (new_end < len(tokens)) and tokens[new_end].startswith('##'):
|
||||
new_end += 1
|
||||
return new_st, new_end
|
||||
|
||||
|
||||
class Pipeline():
|
||||
""" Pre-process Pipeline Class : callable """
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.skipgram_prb = None
|
||||
self.skipgram_size = None
|
||||
self.pre_whole_word = None
|
||||
self.mask_whole_word = None
|
||||
self.word_subsample_prb = None
|
||||
self.sp_prob = None
|
||||
self.pieces_dir = None
|
||||
self.vocab_words = None
|
||||
self.pieces_threshold = 10
|
||||
self.trie = None
|
||||
self.call_count = 0
|
||||
self.offline_mode = False
|
||||
self.skipgram_size_geo_list = None
|
||||
self.span_same_mask = False
|
||||
|
||||
def init_skipgram_size_geo_list(self, p):
|
||||
if p > 0:
|
||||
g_list = []
|
||||
t = p
|
||||
for _ in range(self.skipgram_size):
|
||||
g_list.append(t)
|
||||
t *= (1-p)
|
||||
s = sum(g_list)
|
||||
self.skipgram_size_geo_list = [x/s for x in g_list]
|
||||
|
||||
def create_trie_tree(self, pieces_dir):
|
||||
print("sp_prob = {}".format(self.sp_prob))
|
||||
print("pieces_threshold = {}".format(self.pieces_threshold))
|
||||
if pieces_dir is not None:
|
||||
self.trie = TrieTree()
|
||||
pieces_files = [pieces_dir]
|
||||
for token in self.vocab_words:
|
||||
self.trie.add([token])
|
||||
for piece_file in pieces_files:
|
||||
print("Load piece file: {}".format(piece_file))
|
||||
with open(piece_file, mode='r', encoding='utf-8') as reader:
|
||||
for line in reader:
|
||||
parts = line.split('\t')
|
||||
if int(parts[-1]) < self.pieces_threshold:
|
||||
pass
|
||||
tokens = []
|
||||
for part in parts[:-1]:
|
||||
tokens.extend(part.split(' '))
|
||||
self.trie.add(tokens)
|
||||
|
||||
def __call__(self, instance):
|
||||
raise NotImplementedError
|
||||
|
||||
# pre_whole_word: tokenize to words before masking
|
||||
# post whole word (--mask_whole_word): expand to words after masking
|
||||
def get_masked_pos(self, tokens, n_pred, add_skipgram=False, mask_segment=None, protect_range=None):
|
||||
if self.pieces_dir is not None and self.trie is None:
|
||||
self.create_trie_tree(self.pieces_dir)
|
||||
if self.pre_whole_word:
|
||||
if self.trie is not None:
|
||||
pieces = self.trie.get_pieces(tokens, 0)
|
||||
|
||||
new_pieces = []
|
||||
for piece in pieces:
|
||||
if len(new_pieces) > 0 and tokens[piece[0]].startswith("##"):
|
||||
new_pieces[-1].extend(piece)
|
||||
else:
|
||||
new_pieces.append(piece)
|
||||
del pieces
|
||||
pieces = new_pieces
|
||||
|
||||
pre_word_split = list(_[-1] for _ in pieces)
|
||||
pre_word_split.append(len(tokens))
|
||||
else:
|
||||
pre_word_split = _get_word_split_index(tokens, 0, len(tokens))
|
||||
index2piece = None
|
||||
else:
|
||||
pre_word_split = list(range(0, len(tokens)+1))
|
||||
|
||||
if self.trie is not None:
|
||||
pieces = self.trie.get_pieces(tokens, 0)
|
||||
|
||||
index2piece = {}
|
||||
for piece in pieces:
|
||||
for index in piece:
|
||||
index2piece[index] = (piece[0], piece[-1])
|
||||
else:
|
||||
index2piece = None
|
||||
|
||||
span_list = list(zip(pre_word_split[:-1], pre_word_split[1:]))
|
||||
|
||||
# candidate positions of masked tokens
|
||||
cand_pos = []
|
||||
special_pos = set()
|
||||
if mask_segment:
|
||||
for i, sp in enumerate(span_list):
|
||||
sp_st, sp_end = sp
|
||||
if (sp_end-sp_st == 1) and tokens[sp_st].endswith('SEP]'):
|
||||
segment_index = i
|
||||
break
|
||||
for i, sp in enumerate(span_list):
|
||||
sp_st, sp_end = sp
|
||||
if (sp_end-sp_st == 1) and (tokens[sp_st].endswith('CLS]') or tokens[sp_st].endswith('SEP]')):
|
||||
special_pos.add(i)
|
||||
else:
|
||||
if mask_segment:
|
||||
if ((i < segment_index) and ('a' in mask_segment)) or ((i > segment_index) and ('b' in mask_segment)):
|
||||
cand_pos.append(i)
|
||||
else:
|
||||
cand_pos.append(i)
|
||||
shuffle(cand_pos)
|
||||
|
||||
masked_pos = set()
|
||||
for i_span in cand_pos:
|
||||
if len(masked_pos) >= n_pred:
|
||||
break
|
||||
cand_st, cand_end = span_list[i_span]
|
||||
if len(masked_pos)+cand_end-cand_st > n_pred:
|
||||
continue
|
||||
if any(p in masked_pos for p in range(cand_st, cand_end)):
|
||||
continue
|
||||
|
||||
n_span = 1
|
||||
if index2piece is not None:
|
||||
p_start, p_end = index2piece[i_span]
|
||||
if p_start < p_end and (rand() < self.sp_prob):
|
||||
# n_span = p_end - p_start + 1
|
||||
st_span, end_span = p_start, p_end + 1
|
||||
else:
|
||||
st_span, end_span = i_span, i_span + 1
|
||||
else:
|
||||
rand_skipgram_size = 0
|
||||
# ngram
|
||||
if self.skipgram_size_geo_list:
|
||||
# sampling ngram size from geometric distribution
|
||||
rand_skipgram_size = np.random.choice(
|
||||
len(self.skipgram_size_geo_list), 1, p=self.skipgram_size_geo_list)[0] + 1
|
||||
else:
|
||||
if add_skipgram and (self.skipgram_prb > 0) and (self.skipgram_size >= 2) and (rand() < self.skipgram_prb):
|
||||
rand_skipgram_size = min(
|
||||
randint(2, self.skipgram_size), len(span_list)-i_span)
|
||||
for n in range(2, rand_skipgram_size+1):
|
||||
tail_st, tail_end = span_list[i_span+n-1]
|
||||
if (tail_end-tail_st == 1) and (tail_st in special_pos):
|
||||
break
|
||||
if len(masked_pos)+tail_end-cand_st > n_pred:
|
||||
break
|
||||
n_span = n
|
||||
st_span, end_span = i_span, i_span + n_span
|
||||
|
||||
if self.mask_whole_word:
|
||||
# pre_whole_word==False: position index of span_list is the same as tokens
|
||||
st_span, end_span = _expand_whole_word(
|
||||
tokens, st_span, end_span)
|
||||
|
||||
# subsampling according to frequency
|
||||
if self.word_subsample_prb:
|
||||
skip_pos = set()
|
||||
if self.pre_whole_word:
|
||||
w_span_list = span_list[st_span:end_span]
|
||||
else:
|
||||
split_idx = _get_word_split_index(
|
||||
tokens, st_span, end_span)
|
||||
w_span_list = list(
|
||||
zip(split_idx[:-1], split_idx[1:]))
|
||||
for i, sp in enumerate(w_span_list):
|
||||
sp_st, sp_end = sp
|
||||
if sp_end-sp_st == 1:
|
||||
w_cat = tokens[sp_st]
|
||||
else:
|
||||
w_cat = ''.join(tokens[sp_st:sp_end])
|
||||
if (w_cat in self.word_subsample_prb) and (rand() < self.word_subsample_prb[w_cat]):
|
||||
for k in range(sp_st, sp_end):
|
||||
skip_pos.add(k)
|
||||
else:
|
||||
skip_pos = None
|
||||
|
||||
for sp in range(st_span, end_span):
|
||||
for mp in range(span_list[sp][0], span_list[sp][1]):
|
||||
if not(skip_pos and (mp in skip_pos)) and (mp not in special_pos) and not(protect_range and (protect_range[0] <= mp < protect_range[1])):
|
||||
masked_pos.add(mp)
|
||||
|
||||
if len(masked_pos) < n_pred:
|
||||
shuffle(cand_pos)
|
||||
for pos in cand_pos:
|
||||
if len(masked_pos) >= n_pred:
|
||||
break
|
||||
if pos not in masked_pos:
|
||||
masked_pos.add(pos)
|
||||
masked_pos = list(masked_pos)
|
||||
if len(masked_pos) > n_pred:
|
||||
# shuffle(masked_pos)
|
||||
masked_pos = masked_pos[:n_pred]
|
||||
return masked_pos
|
||||
|
||||
def replace_masked_tokens(self, tokens, masked_pos):
|
||||
if self.span_same_mask:
|
||||
masked_pos = sorted(list(masked_pos))
|
||||
prev_pos, prev_rand = None, None
|
||||
for pos in masked_pos:
|
||||
if self.span_same_mask and (pos-1 == prev_pos):
|
||||
t_rand = prev_rand
|
||||
else:
|
||||
t_rand = rand()
|
||||
if t_rand < 0.8: # 80%
|
||||
tokens[pos] = '[MASK]'
|
||||
elif t_rand < 0.9: # 10%
|
||||
tokens[pos] = get_random_word(self.vocab_words)
|
||||
prev_pos, prev_rand = pos, t_rand
|
||||
@@ -0,0 +1,483 @@
|
||||
"""BERT finetuning runner."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import logging
|
||||
import glob
|
||||
import math
|
||||
import json
|
||||
import argparse
|
||||
import random
|
||||
from pathlib import Path
|
||||
from tqdm import tqdm, trange
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.utils.data import RandomSampler
|
||||
from torch.utils.data.distributed import DistributedSampler
|
||||
|
||||
from pytorch_pretrained_bert.tokenization import BertTokenizer, WhitespaceTokenizer
|
||||
from pytorch_pretrained_bert.modeling import BertForPreTrainingLossMask
|
||||
from pytorch_pretrained_bert.optimization import BertAdam, warmup_linear
|
||||
|
||||
from nn.data_parallel import DataParallelImbalance
|
||||
import biunilm.seq2seq_loader as seq2seq_loader
|
||||
import torch.distributed as dist
|
||||
|
||||
|
||||
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
|
||||
datefmt='%m/%d/%Y %H:%M:%S',
|
||||
level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_max_epoch_model(output_dir):
|
||||
fn_model_list = glob.glob(os.path.join(output_dir, "model.*.bin"))
|
||||
fn_optim_list = glob.glob(os.path.join(output_dir, "optim.*.bin"))
|
||||
if (not fn_model_list) or (not fn_optim_list):
|
||||
return None
|
||||
both_set = set([int(Path(fn).stem.split('.')[-1]) for fn in fn_model_list]
|
||||
) & set([int(Path(fn).stem.split('.')[-1]) for fn in fn_optim_list])
|
||||
if both_set:
|
||||
return max(both_set)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# Required parameters
|
||||
parser.add_argument("--data_dir",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="The input data dir. Should contain the .tsv files (or other data files) for the task.")
|
||||
parser.add_argument("--src_file", default=None, type=str,
|
||||
help="The input data file name.")
|
||||
parser.add_argument("--tgt_file", default=None, type=str,
|
||||
help="The output data file name.")
|
||||
parser.add_argument("--bert_model", default=None, type=str, required=True,
|
||||
help="Bert pre-trained model selected in the list: bert-base-uncased, "
|
||||
"bert-large-uncased, bert-base-cased, bert-base-multilingual, bert-base-chinese.")
|
||||
parser.add_argument("--config_path", default=None, type=str,
|
||||
help="Bert config file path.")
|
||||
parser.add_argument("--output_dir",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="The output directory where the model predictions and checkpoints will be written.")
|
||||
parser.add_argument("--log_dir",
|
||||
default='',
|
||||
type=str,
|
||||
required=True,
|
||||
help="The output directory where the log will be written.")
|
||||
parser.add_argument("--model_recover_path",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="The file of fine-tuned pretraining model.")
|
||||
parser.add_argument("--optim_recover_path",
|
||||
default=None,
|
||||
type=str,
|
||||
help="The file of pretraining optimizer.")
|
||||
|
||||
# Other parameters
|
||||
parser.add_argument("--max_seq_length",
|
||||
default=128,
|
||||
type=int,
|
||||
help="The maximum total input sequence length after WordPiece tokenization. \n"
|
||||
"Sequences longer than this will be truncated, and sequences shorter \n"
|
||||
"than this will be padded.")
|
||||
parser.add_argument("--do_train",
|
||||
action='store_true',
|
||||
help="Whether to run training.")
|
||||
parser.add_argument("--do_eval",
|
||||
action='store_true',
|
||||
help="Whether to run eval on the dev set.")
|
||||
parser.add_argument("--do_lower_case",
|
||||
action='store_true',
|
||||
help="Set this flag if you are using an uncased model.")
|
||||
parser.add_argument("--train_batch_size",
|
||||
default=32,
|
||||
type=int,
|
||||
help="Total batch size for training.")
|
||||
parser.add_argument("--eval_batch_size",
|
||||
default=64,
|
||||
type=int,
|
||||
help="Total batch size for eval.")
|
||||
parser.add_argument("--learning_rate", default=5e-5, type=float,
|
||||
help="The initial learning rate for Adam.")
|
||||
parser.add_argument("--label_smoothing", default=0, type=float,
|
||||
help="The initial learning rate for Adam.")
|
||||
parser.add_argument("--weight_decay",
|
||||
default=0.01,
|
||||
type=float,
|
||||
help="The weight decay rate for Adam.")
|
||||
parser.add_argument("--finetune_decay",
|
||||
action='store_true',
|
||||
help="Weight decay to the original weights.")
|
||||
parser.add_argument("--num_train_epochs",
|
||||
default=3.0,
|
||||
type=float,
|
||||
help="Total number of training epochs to perform.")
|
||||
parser.add_argument("--warmup_proportion",
|
||||
default=0.1,
|
||||
type=float,
|
||||
help="Proportion of training to perform linear learning rate warmup for. "
|
||||
"E.g., 0.1 = 10%% of training.")
|
||||
parser.add_argument("--hidden_dropout_prob", default=0.1, type=float,
|
||||
help="Dropout rate for hidden states.")
|
||||
parser.add_argument("--attention_probs_dropout_prob", default=0.1, type=float,
|
||||
help="Dropout rate for attention probabilities.")
|
||||
parser.add_argument("--no_cuda",
|
||||
action='store_true',
|
||||
help="Whether not to use CUDA when available")
|
||||
parser.add_argument("--local_rank",
|
||||
type=int,
|
||||
default=-1,
|
||||
help="local_rank for distributed training on gpus")
|
||||
parser.add_argument('--seed',
|
||||
type=int,
|
||||
default=42,
|
||||
help="random seed for initialization")
|
||||
parser.add_argument('--gradient_accumulation_steps',
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of updates steps to accumulate before performing a backward/update pass.")
|
||||
parser.add_argument('--fp16', action='store_true',
|
||||
help="Whether to use 16-bit float precision instead of 32-bit")
|
||||
parser.add_argument('--fp32_embedding', action='store_true',
|
||||
help="Whether to use 32-bit float precision instead of 16-bit for embeddings")
|
||||
parser.add_argument('--loss_scale', type=float, default=0,
|
||||
help="Loss scaling to improve fp16 numeric stability. Only used when fp16 set to True.\n"
|
||||
"0 (default value): dynamic loss scaling.\n"
|
||||
"Positive power of 2: static loss scaling value.\n")
|
||||
parser.add_argument('--amp', action='store_true',
|
||||
help="Whether to use amp for fp16")
|
||||
parser.add_argument('--from_scratch', action='store_true',
|
||||
help="Initialize parameters with random values (i.e., training from scratch).")
|
||||
parser.add_argument('--new_segment_ids', action='store_true',
|
||||
help="Use new segment ids for bi-uni-directional LM.")
|
||||
parser.add_argument('--new_pos_ids', action='store_true',
|
||||
help="Use new position ids for LMs.")
|
||||
parser.add_argument('--tokenized_input', action='store_true',
|
||||
help="Whether the input is tokenized.")
|
||||
parser.add_argument('--max_len_a', type=int, default=0,
|
||||
help="Truncate_config: maximum length of segment A.")
|
||||
parser.add_argument('--max_len_b', type=int, default=0,
|
||||
help="Truncate_config: maximum length of segment B.")
|
||||
parser.add_argument('--trunc_seg', default='',
|
||||
help="Truncate_config: first truncate segment A/B (option: a, b).")
|
||||
parser.add_argument('--always_truncate_tail', action='store_true',
|
||||
help="Truncate_config: Whether we should always truncate tail.")
|
||||
parser.add_argument("--mask_prob", default=0.15, type=float,
|
||||
help="Number of prediction is sometimes less than max_pred when sequence is short.")
|
||||
parser.add_argument("--mask_prob_eos", default=0, type=float,
|
||||
help="Number of prediction is sometimes less than max_pred when sequence is short.")
|
||||
parser.add_argument('--max_pred', type=int, default=20,
|
||||
help="Max tokens of prediction.")
|
||||
parser.add_argument("--num_workers", default=0, type=int,
|
||||
help="Number of workers for the data loader.")
|
||||
|
||||
parser.add_argument('--mask_source_words', action='store_true',
|
||||
help="Whether to mask source words for training")
|
||||
parser.add_argument('--skipgram_prb', type=float, default=0.0,
|
||||
help='prob of ngram mask')
|
||||
parser.add_argument('--skipgram_size', type=int, default=1,
|
||||
help='the max size of ngram mask')
|
||||
parser.add_argument('--mask_whole_word', action='store_true',
|
||||
help="Whether masking a whole word.")
|
||||
parser.add_argument('--do_l2r_training', action='store_true',
|
||||
help="Whether to do left to right training")
|
||||
parser.add_argument('--has_sentence_oracle', action='store_true',
|
||||
help="Whether to have sentence level oracle for training. "
|
||||
"Only useful for summary generation")
|
||||
parser.add_argument('--max_position_embeddings', type=int, default=None,
|
||||
help="max position embeddings")
|
||||
parser.add_argument('--relax_projection', action='store_true',
|
||||
help="Use different projection layers for tasks.")
|
||||
parser.add_argument('--ffn_type', default=0, type=int,
|
||||
help="0: default mlp; 1: W((Wx+b) elem_prod x);")
|
||||
parser.add_argument('--num_qkv', default=0, type=int,
|
||||
help="Number of different <Q,K,V>.")
|
||||
parser.add_argument('--seg_emb', action='store_true',
|
||||
help="Using segment embedding for self-attention.")
|
||||
parser.add_argument('--s2s_special_token', action='store_true',
|
||||
help="New special tokens ([S2S_SEP]/[S2S_CLS]) of S2S.")
|
||||
parser.add_argument('--s2s_add_segment', action='store_true',
|
||||
help="Additional segmental for the encoder of S2S.")
|
||||
parser.add_argument('--s2s_share_segment', action='store_true',
|
||||
help="Sharing segment embeddings for the encoder of S2S (used with --s2s_add_segment).")
|
||||
parser.add_argument('--pos_shift', action='store_true',
|
||||
help="Using position shift for fine-tuning.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
assert Path(args.model_recover_path).exists(
|
||||
), "--model_recover_path doesn't exist"
|
||||
|
||||
args.output_dir = args.output_dir.replace(
|
||||
'[PT_OUTPUT_DIR]', os.getenv('PT_OUTPUT_DIR', ''))
|
||||
args.log_dir = args.log_dir.replace(
|
||||
'[PT_OUTPUT_DIR]', os.getenv('PT_OUTPUT_DIR', ''))
|
||||
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
os.makedirs(args.log_dir, exist_ok=True)
|
||||
json.dump(args.__dict__, open(os.path.join(
|
||||
args.output_dir, 'opt.json'), 'w'), sort_keys=True, indent=2)
|
||||
|
||||
if args.local_rank == -1 or args.no_cuda:
|
||||
device = torch.device(
|
||||
"cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
|
||||
n_gpu = torch.cuda.device_count()
|
||||
else:
|
||||
torch.cuda.set_device(args.local_rank)
|
||||
device = torch.device("cuda", args.local_rank)
|
||||
n_gpu = 1
|
||||
# Initializes the distributed backend which will take care of sychronizing nodes/GPUs
|
||||
dist.init_process_group(backend='nccl')
|
||||
logger.info("device: {} n_gpu: {}, distributed training: {}, 16-bits training: {}".format(
|
||||
device, n_gpu, bool(args.local_rank != -1), args.fp16))
|
||||
|
||||
if args.gradient_accumulation_steps < 1:
|
||||
raise ValueError("Invalid gradient_accumulation_steps parameter: {}, should be >= 1".format(
|
||||
args.gradient_accumulation_steps))
|
||||
|
||||
args.train_batch_size = int(
|
||||
args.train_batch_size / args.gradient_accumulation_steps)
|
||||
|
||||
random.seed(args.seed)
|
||||
np.random.seed(args.seed)
|
||||
torch.manual_seed(args.seed)
|
||||
if n_gpu > 0:
|
||||
torch.cuda.manual_seed_all(args.seed)
|
||||
|
||||
if not args.do_train and not args.do_eval:
|
||||
raise ValueError(
|
||||
"At least one of `do_train` or `do_eval` must be True.")
|
||||
|
||||
if args.local_rank not in (-1, 0):
|
||||
# Make sure only the first process in distributed training will download model & vocab
|
||||
dist.barrier()
|
||||
tokenizer = BertTokenizer.from_pretrained(
|
||||
args.bert_model, do_lower_case=args.do_lower_case)
|
||||
if args.max_position_embeddings:
|
||||
tokenizer.max_len = args.max_position_embeddings
|
||||
data_tokenizer = WhitespaceTokenizer() if args.tokenized_input else tokenizer
|
||||
if args.local_rank == 0:
|
||||
dist.barrier()
|
||||
|
||||
if args.do_train:
|
||||
print("Loading Train Dataset", args.data_dir)
|
||||
bi_uni_pipeline = [seq2seq_loader.Preprocess4Seq2seq(args.max_pred, args.mask_prob, list(tokenizer.vocab.keys(
|
||||
)), tokenizer.convert_tokens_to_ids, args.max_seq_length, new_segment_ids=args.new_segment_ids, truncate_config={'max_len_a': args.max_len_a, 'max_len_b': args.max_len_b, 'trunc_seg': args.trunc_seg, 'always_truncate_tail': args.always_truncate_tail}, mask_source_words=args.mask_source_words, skipgram_prb=args.skipgram_prb, skipgram_size=args.skipgram_size, mask_whole_word=args.mask_whole_word, mode="s2s", has_oracle=args.has_sentence_oracle, num_qkv=args.num_qkv, s2s_special_token=args.s2s_special_token, s2s_add_segment=args.s2s_add_segment, s2s_share_segment=args.s2s_share_segment, pos_shift=args.pos_shift)]
|
||||
file_oracle = None
|
||||
if args.has_sentence_oracle:
|
||||
file_oracle = os.path.join(args.data_dir, 'train.oracle')
|
||||
fn_src = os.path.join(
|
||||
args.data_dir, args.src_file if args.src_file else 'train.src')
|
||||
fn_tgt = os.path.join(
|
||||
args.data_dir, args.tgt_file if args.tgt_file else 'train.tgt')
|
||||
train_dataset = seq2seq_loader.Seq2SeqDataset(
|
||||
fn_src, fn_tgt, args.train_batch_size, data_tokenizer, args.max_seq_length, file_oracle=file_oracle, bi_uni_pipeline=bi_uni_pipeline)
|
||||
if args.local_rank == -1:
|
||||
train_sampler = RandomSampler(train_dataset, replacement=False)
|
||||
_batch_size = args.train_batch_size
|
||||
else:
|
||||
train_sampler = DistributedSampler(train_dataset)
|
||||
_batch_size = args.train_batch_size // dist.get_world_size()
|
||||
train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=_batch_size, sampler=train_sampler,
|
||||
num_workers=args.num_workers, collate_fn=seq2seq_loader.batch_list_to_batch_tensors, pin_memory=False)
|
||||
|
||||
# note: args.train_batch_size has been changed to (/= args.gradient_accumulation_steps)
|
||||
# t_total = int(math.ceil(len(train_dataset.ex_list) / args.train_batch_size)
|
||||
t_total = int(len(train_dataloader) * args.num_train_epochs /
|
||||
args.gradient_accumulation_steps)
|
||||
|
||||
amp_handle = None
|
||||
if args.fp16 and args.amp:
|
||||
from apex import amp
|
||||
amp_handle = amp.init(enable_caching=True)
|
||||
logger.info("enable fp16 with amp")
|
||||
|
||||
# Prepare model
|
||||
recover_step = _get_max_epoch_model(args.output_dir)
|
||||
cls_num_labels = 2
|
||||
type_vocab_size = 6 + \
|
||||
(1 if args.s2s_add_segment else 0) if args.new_segment_ids else 2
|
||||
num_sentlvl_labels = 2 if args.has_sentence_oracle else 0
|
||||
relax_projection = 4 if args.relax_projection else 0
|
||||
if args.local_rank not in (-1, 0):
|
||||
# Make sure only the first process in distributed training will download model & vocab
|
||||
dist.barrier()
|
||||
if (recover_step is None) and (args.model_recover_path is None):
|
||||
# if _state_dict == {}, the parameters are randomly initialized
|
||||
# if _state_dict == None, the parameters are initialized with bert-init
|
||||
_state_dict = {} if args.from_scratch else None
|
||||
model = BertForPreTrainingLossMask.from_pretrained(
|
||||
args.bert_model, state_dict=_state_dict, num_labels=cls_num_labels, num_rel=0, type_vocab_size=type_vocab_size, config_path=args.config_path, task_idx=3, num_sentlvl_labels=num_sentlvl_labels, max_position_embeddings=args.max_position_embeddings, label_smoothing=args.label_smoothing, fp32_embedding=args.fp32_embedding, relax_projection=relax_projection, new_pos_ids=args.new_pos_ids, ffn_type=args.ffn_type, hidden_dropout_prob=args.hidden_dropout_prob, attention_probs_dropout_prob=args.attention_probs_dropout_prob, num_qkv=args.num_qkv, seg_emb=args.seg_emb)
|
||||
global_step = 0
|
||||
else:
|
||||
if recover_step:
|
||||
logger.info("***** Recover model: %d *****", recover_step)
|
||||
model_recover = torch.load(os.path.join(
|
||||
args.output_dir, "model.{0}.bin".format(recover_step)), map_location='cpu')
|
||||
# recover_step == number of epochs
|
||||
global_step = math.floor(
|
||||
recover_step * t_total / args.num_train_epochs)
|
||||
elif args.model_recover_path:
|
||||
logger.info("***** Recover model: %s *****",
|
||||
args.model_recover_path)
|
||||
model_recover = torch.load(
|
||||
args.model_recover_path, map_location='cpu')
|
||||
global_step = 0
|
||||
model = BertForPreTrainingLossMask.from_pretrained(
|
||||
args.bert_model, state_dict=model_recover, num_labels=cls_num_labels, num_rel=0, type_vocab_size=type_vocab_size, config_path=args.config_path, task_idx=3, num_sentlvl_labels=num_sentlvl_labels, max_position_embeddings=args.max_position_embeddings, label_smoothing=args.label_smoothing, fp32_embedding=args.fp32_embedding, relax_projection=relax_projection, new_pos_ids=args.new_pos_ids, ffn_type=args.ffn_type, hidden_dropout_prob=args.hidden_dropout_prob, attention_probs_dropout_prob=args.attention_probs_dropout_prob, num_qkv=args.num_qkv, seg_emb=args.seg_emb)
|
||||
if args.local_rank == 0:
|
||||
dist.barrier()
|
||||
|
||||
if args.fp16:
|
||||
model.half()
|
||||
if args.fp32_embedding:
|
||||
model.bert.embeddings.word_embeddings.float()
|
||||
model.bert.embeddings.position_embeddings.float()
|
||||
model.bert.embeddings.token_type_embeddings.float()
|
||||
model.to(device)
|
||||
if args.local_rank != -1:
|
||||
try:
|
||||
from torch.nn.parallel import DistributedDataParallel as DDP
|
||||
except ImportError:
|
||||
raise ImportError("DistributedDataParallel")
|
||||
model = DDP(model, device_ids=[
|
||||
args.local_rank], output_device=args.local_rank, find_unused_parameters=True)
|
||||
elif n_gpu > 1:
|
||||
# model = torch.nn.DataParallel(model)
|
||||
model = DataParallelImbalance(model)
|
||||
|
||||
# Prepare optimizer
|
||||
param_optimizer = list(model.named_parameters())
|
||||
no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
|
||||
optimizer_grouped_parameters = [
|
||||
{'params': [p for n, p in param_optimizer if not any(
|
||||
nd in n for nd in no_decay)], 'weight_decay': 0.01},
|
||||
{'params': [p for n, p in param_optimizer if any(
|
||||
nd in n for nd in no_decay)], 'weight_decay': 0.0}
|
||||
]
|
||||
if args.fp16:
|
||||
try:
|
||||
# from apex.optimizers import FP16_Optimizer
|
||||
from pytorch_pretrained_bert.optimization_fp16 import FP16_Optimizer_State
|
||||
from apex.optimizers import FusedAdam
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install apex from https://www.github.com/nvidia/apex to use distributed and fp16 training.")
|
||||
|
||||
optimizer = FusedAdam(optimizer_grouped_parameters,
|
||||
lr=args.learning_rate,
|
||||
bias_correction=False,
|
||||
max_grad_norm=1.0)
|
||||
if args.loss_scale == 0:
|
||||
optimizer = FP16_Optimizer_State(
|
||||
optimizer, dynamic_loss_scale=True)
|
||||
else:
|
||||
optimizer = FP16_Optimizer_State(
|
||||
optimizer, static_loss_scale=args.loss_scale)
|
||||
else:
|
||||
optimizer = BertAdam(optimizer_grouped_parameters,
|
||||
lr=args.learning_rate,
|
||||
warmup=args.warmup_proportion,
|
||||
t_total=t_total)
|
||||
|
||||
if recover_step:
|
||||
logger.info("***** Recover optimizer: %d *****", recover_step)
|
||||
optim_recover = torch.load(os.path.join(
|
||||
args.output_dir, "optim.{0}.bin".format(recover_step)), map_location='cpu')
|
||||
if hasattr(optim_recover, 'state_dict'):
|
||||
optim_recover = optim_recover.state_dict()
|
||||
optimizer.load_state_dict(optim_recover)
|
||||
if args.loss_scale == 0:
|
||||
logger.info("***** Recover optimizer: dynamic_loss_scale *****")
|
||||
optimizer.dynamic_loss_scale = True
|
||||
|
||||
logger.info("***** CUDA.empty_cache() *****")
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
if args.do_train:
|
||||
logger.info("***** Running training *****")
|
||||
logger.info(" Batch size = %d", args.train_batch_size)
|
||||
logger.info(" Num steps = %d", t_total)
|
||||
|
||||
model.train()
|
||||
if recover_step:
|
||||
start_epoch = recover_step+1
|
||||
else:
|
||||
start_epoch = 1
|
||||
for i_epoch in trange(start_epoch, int(args.num_train_epochs)+1, desc="Epoch", disable=args.local_rank not in (-1, 0)):
|
||||
if args.local_rank != -1:
|
||||
train_sampler.set_epoch(i_epoch)
|
||||
iter_bar = tqdm(train_dataloader, desc='Iter (loss=X.XXX)',
|
||||
disable=args.local_rank not in (-1, 0))
|
||||
for step, batch in enumerate(iter_bar):
|
||||
batch = [
|
||||
t.to(device) if t is not None else None for t in batch]
|
||||
if args.has_sentence_oracle:
|
||||
input_ids, segment_ids, input_mask, mask_qkv, lm_label_ids, masked_pos, masked_weights, is_next, task_idx, oracle_pos, oracle_weights, oracle_labels = batch
|
||||
else:
|
||||
input_ids, segment_ids, input_mask, mask_qkv, lm_label_ids, masked_pos, masked_weights, is_next, task_idx = batch
|
||||
oracle_pos, oracle_weights, oracle_labels = None, None, None
|
||||
loss_tuple = model(input_ids, segment_ids, input_mask, lm_label_ids, is_next, masked_pos=masked_pos, masked_weights=masked_weights, task_idx=task_idx, masked_pos_2=oracle_pos, masked_weights_2=oracle_weights,
|
||||
masked_labels_2=oracle_labels, mask_qkv=mask_qkv)
|
||||
masked_lm_loss, next_sentence_loss = loss_tuple
|
||||
if n_gpu > 1: # mean() to average on multi-gpu.
|
||||
# loss = loss.mean()
|
||||
masked_lm_loss = masked_lm_loss.mean()
|
||||
next_sentence_loss = next_sentence_loss.mean()
|
||||
loss = masked_lm_loss + next_sentence_loss
|
||||
|
||||
# logging for each step (i.e., before normalization by args.gradient_accumulation_steps)
|
||||
iter_bar.set_description('Iter (loss=%5.3f)' % loss.item())
|
||||
|
||||
# ensure that accumlated gradients are normalized
|
||||
if args.gradient_accumulation_steps > 1:
|
||||
loss = loss / args.gradient_accumulation_steps
|
||||
|
||||
if args.fp16:
|
||||
optimizer.backward(loss)
|
||||
if amp_handle:
|
||||
amp_handle._clear_cache()
|
||||
else:
|
||||
loss.backward()
|
||||
if (step + 1) % args.gradient_accumulation_steps == 0:
|
||||
lr_this_step = args.learning_rate * \
|
||||
warmup_linear(global_step/t_total,
|
||||
args.warmup_proportion)
|
||||
if args.fp16:
|
||||
# modify learning rate with special warm up BERT uses
|
||||
for param_group in optimizer.param_groups:
|
||||
param_group['lr'] = lr_this_step
|
||||
optimizer.step()
|
||||
optimizer.zero_grad()
|
||||
global_step += 1
|
||||
|
||||
# Save a trained model
|
||||
if (args.local_rank == -1 or torch.distributed.get_rank() == 0):
|
||||
logger.info(
|
||||
"** ** * Saving fine-tuned model and optimizer ** ** * ")
|
||||
model_to_save = model.module if hasattr(
|
||||
model, 'module') else model # Only save the model it-self
|
||||
output_model_file = os.path.join(
|
||||
args.output_dir, "model.{0}.bin".format(i_epoch))
|
||||
torch.save(model_to_save.state_dict(), output_model_file)
|
||||
output_optim_file = os.path.join(
|
||||
args.output_dir, "optim.{0}.bin".format(i_epoch))
|
||||
torch.save(optimizer.state_dict(), output_optim_file)
|
||||
|
||||
logger.info("***** CUDA.empty_cache() *****")
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,407 @@
|
||||
from random import randint, shuffle, choice
|
||||
from random import random as rand
|
||||
import math
|
||||
import torch
|
||||
|
||||
from biunilm.loader_utils import get_random_word, batch_list_to_batch_tensors, Pipeline
|
||||
|
||||
# Input file format :
|
||||
# 1. One sentence per line. These should ideally be actual sentences,
|
||||
# not entire paragraphs or arbitrary spans of text. (Because we use
|
||||
# the sentence boundaries for the "next sentence prediction" task).
|
||||
# 2. Blank lines between documents. Document boundaries are needed
|
||||
# so that the "next sentence prediction" task doesn't span between documents.
|
||||
|
||||
|
||||
def truncate_tokens_pair(tokens_a, tokens_b, max_len, max_len_a=0, max_len_b=0, trunc_seg=None, always_truncate_tail=False):
|
||||
num_truncated_a = [0, 0]
|
||||
num_truncated_b = [0, 0]
|
||||
while True:
|
||||
if len(tokens_a) + len(tokens_b) <= max_len:
|
||||
break
|
||||
if (max_len_a > 0) and len(tokens_a) > max_len_a:
|
||||
trunc_tokens = tokens_a
|
||||
num_truncated = num_truncated_a
|
||||
elif (max_len_b > 0) and len(tokens_b) > max_len_b:
|
||||
trunc_tokens = tokens_b
|
||||
num_truncated = num_truncated_b
|
||||
elif trunc_seg:
|
||||
# truncate the specified segment
|
||||
if trunc_seg == 'a':
|
||||
trunc_tokens = tokens_a
|
||||
num_truncated = num_truncated_a
|
||||
else:
|
||||
trunc_tokens = tokens_b
|
||||
num_truncated = num_truncated_b
|
||||
else:
|
||||
# truncate the longer segment
|
||||
if len(tokens_a) > len(tokens_b):
|
||||
trunc_tokens = tokens_a
|
||||
num_truncated = num_truncated_a
|
||||
else:
|
||||
trunc_tokens = tokens_b
|
||||
num_truncated = num_truncated_b
|
||||
# whether always truncate source sequences
|
||||
if (not always_truncate_tail) and (rand() < 0.5):
|
||||
del trunc_tokens[0]
|
||||
num_truncated[0] += 1
|
||||
else:
|
||||
trunc_tokens.pop()
|
||||
num_truncated[1] += 1
|
||||
return num_truncated_a, num_truncated_b
|
||||
|
||||
|
||||
class Seq2SeqDataset(torch.utils.data.Dataset):
|
||||
""" Load sentence pair (sequential or random order) from corpus """
|
||||
|
||||
def __init__(self, file_src, file_tgt, batch_size, tokenizer, max_len, file_oracle=None, short_sampling_prob=0.1, sent_reverse_order=False, bi_uni_pipeline=[]):
|
||||
super().__init__()
|
||||
self.tokenizer = tokenizer # tokenize function
|
||||
self.max_len = max_len # maximum length of tokens
|
||||
self.short_sampling_prob = short_sampling_prob
|
||||
self.bi_uni_pipeline = bi_uni_pipeline
|
||||
self.batch_size = batch_size
|
||||
self.sent_reverse_order = sent_reverse_order
|
||||
|
||||
# read the file into memory
|
||||
self.ex_list = []
|
||||
if file_oracle is None:
|
||||
with open(file_src, "r", encoding='utf-8') as f_src, open(file_tgt, "r", encoding='utf-8') as f_tgt:
|
||||
for src, tgt in zip(f_src, f_tgt):
|
||||
src_tk = tokenizer.tokenize(src.strip())
|
||||
tgt_tk = tokenizer.tokenize(tgt.strip())
|
||||
assert len(src_tk) > 0
|
||||
assert len(tgt_tk) > 0
|
||||
self.ex_list.append((src_tk, tgt_tk))
|
||||
else:
|
||||
with open(file_src, "r", encoding='utf-8') as f_src, \
|
||||
open(file_tgt, "r", encoding='utf-8') as f_tgt, \
|
||||
open(file_oracle, "r", encoding='utf-8') as f_orc:
|
||||
for src, tgt, orc in zip(f_src, f_tgt, f_orc):
|
||||
src_tk = tokenizer.tokenize(src.strip())
|
||||
tgt_tk = tokenizer.tokenize(tgt.strip())
|
||||
s_st, labl = orc.split('\t')
|
||||
s_st = [int(x) for x in s_st.split()]
|
||||
labl = [int(x) for x in labl.split()]
|
||||
self.ex_list.append((src_tk, tgt_tk, s_st, labl))
|
||||
print('Load {0} documents'.format(len(self.ex_list)))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.ex_list)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
instance = self.ex_list[idx]
|
||||
proc = choice(self.bi_uni_pipeline)
|
||||
instance = proc(instance)
|
||||
return instance
|
||||
|
||||
def __iter__(self): # iterator to load data
|
||||
for __ in range(math.ceil(len(self.ex_list) / float(self.batch_size))):
|
||||
batch = []
|
||||
for __ in range(self.batch_size):
|
||||
idx = randint(0, len(self.ex_list)-1)
|
||||
batch.append(self.__getitem__(idx))
|
||||
# To Tensor
|
||||
yield batch_list_to_batch_tensors(batch)
|
||||
|
||||
|
||||
class Preprocess4Seq2seq(Pipeline):
|
||||
""" Pre-processing steps for pretraining transformer """
|
||||
|
||||
def __init__(self, max_pred, mask_prob, vocab_words, indexer, max_len=512, skipgram_prb=0, skipgram_size=0, block_mask=False, mask_whole_word=False, new_segment_ids=False, truncate_config={}, mask_source_words=False, mode="s2s", has_oracle=False, num_qkv=0, s2s_special_token=False, s2s_add_segment=False, s2s_share_segment=False, pos_shift=False):
|
||||
super().__init__()
|
||||
self.max_len = max_len
|
||||
self.max_pred = max_pred # max tokens of prediction
|
||||
self.mask_prob = mask_prob # masking probability
|
||||
self.vocab_words = vocab_words # vocabulary (sub)words
|
||||
self.indexer = indexer # function from token to token index
|
||||
self.max_len = max_len
|
||||
self._tril_matrix = torch.tril(torch.ones(
|
||||
(max_len, max_len), dtype=torch.long))
|
||||
self.skipgram_prb = skipgram_prb
|
||||
self.skipgram_size = skipgram_size
|
||||
self.mask_whole_word = mask_whole_word
|
||||
self.new_segment_ids = new_segment_ids
|
||||
self.always_truncate_tail = truncate_config.get(
|
||||
'always_truncate_tail', False)
|
||||
self.max_len_a = truncate_config.get('max_len_a', None)
|
||||
self.max_len_b = truncate_config.get('max_len_b', None)
|
||||
self.trunc_seg = truncate_config.get('trunc_seg', None)
|
||||
self.task_idx = 3 # relax projection layer for different tasks
|
||||
self.mask_source_words = mask_source_words
|
||||
assert mode in ("s2s", "l2r")
|
||||
self.mode = mode
|
||||
self.has_oracle = has_oracle
|
||||
self.num_qkv = num_qkv
|
||||
self.s2s_special_token = s2s_special_token
|
||||
self.s2s_add_segment = s2s_add_segment
|
||||
self.s2s_share_segment = s2s_share_segment
|
||||
self.pos_shift = pos_shift
|
||||
|
||||
def __call__(self, instance):
|
||||
tokens_a, tokens_b = instance[:2]
|
||||
|
||||
if self.pos_shift:
|
||||
tokens_b = ['[S2S_SOS]'] + tokens_b
|
||||
|
||||
# -3 for special tokens [CLS], [SEP], [SEP]
|
||||
num_truncated_a, _ = truncate_tokens_pair(tokens_a, tokens_b, self.max_len - 3, max_len_a=self.max_len_a,
|
||||
max_len_b=self.max_len_b, trunc_seg=self.trunc_seg, always_truncate_tail=self.always_truncate_tail)
|
||||
|
||||
# Add Special Tokens
|
||||
if self.s2s_special_token:
|
||||
tokens = ['[S2S_CLS]'] + tokens_a + \
|
||||
['[S2S_SEP]'] + tokens_b + ['[SEP]']
|
||||
else:
|
||||
tokens = ['[CLS]'] + tokens_a + ['[SEP]'] + tokens_b + ['[SEP]']
|
||||
|
||||
if self.new_segment_ids:
|
||||
if self.mode == "s2s":
|
||||
if self.s2s_add_segment:
|
||||
if self.s2s_share_segment:
|
||||
segment_ids = [0] + [1] * \
|
||||
(len(tokens_a)+1) + [5]*(len(tokens_b)+1)
|
||||
else:
|
||||
segment_ids = [4] + [6] * \
|
||||
(len(tokens_a)+1) + [5]*(len(tokens_b)+1)
|
||||
else:
|
||||
segment_ids = [4] * (len(tokens_a)+2) + \
|
||||
[5]*(len(tokens_b)+1)
|
||||
else:
|
||||
segment_ids = [2] * (len(tokens))
|
||||
else:
|
||||
segment_ids = [0]*(len(tokens_a)+2) + [1]*(len(tokens_b)+1)
|
||||
|
||||
if self.pos_shift:
|
||||
n_pred = min(self.max_pred, len(tokens_b))
|
||||
masked_pos = [len(tokens_a)+2+i for i in range(len(tokens_b))]
|
||||
masked_weights = [1]*n_pred
|
||||
masked_ids = self.indexer(tokens_b[1:]+['[SEP]'])
|
||||
else:
|
||||
# For masked Language Models
|
||||
# the number of prediction is sometimes less than max_pred when sequence is short
|
||||
effective_length = len(tokens_b)
|
||||
if self.mask_source_words:
|
||||
effective_length += len(tokens_a)
|
||||
n_pred = min(self.max_pred, max(
|
||||
1, int(round(effective_length*self.mask_prob))))
|
||||
# candidate positions of masked tokens
|
||||
cand_pos = []
|
||||
special_pos = set()
|
||||
for i, tk in enumerate(tokens):
|
||||
# only mask tokens_b (target sequence)
|
||||
# we will mask [SEP] as an ending symbol
|
||||
if (i >= len(tokens_a)+2) and (tk != '[CLS]'):
|
||||
cand_pos.append(i)
|
||||
elif self.mask_source_words and (i < len(tokens_a)+2) and (tk != '[CLS]') and (not tk.startswith('[SEP')):
|
||||
cand_pos.append(i)
|
||||
else:
|
||||
special_pos.add(i)
|
||||
shuffle(cand_pos)
|
||||
|
||||
masked_pos = set()
|
||||
max_cand_pos = max(cand_pos)
|
||||
for pos in cand_pos:
|
||||
if len(masked_pos) >= n_pred:
|
||||
break
|
||||
if pos in masked_pos:
|
||||
continue
|
||||
|
||||
def _expand_whole_word(st, end):
|
||||
new_st, new_end = st, end
|
||||
while (new_st >= 0) and tokens[new_st].startswith('##'):
|
||||
new_st -= 1
|
||||
while (new_end < len(tokens)) and tokens[new_end].startswith('##'):
|
||||
new_end += 1
|
||||
return new_st, new_end
|
||||
|
||||
if (self.skipgram_prb > 0) and (self.skipgram_size >= 2) and (rand() < self.skipgram_prb):
|
||||
# ngram
|
||||
cur_skipgram_size = randint(2, self.skipgram_size)
|
||||
if self.mask_whole_word:
|
||||
st_pos, end_pos = _expand_whole_word(
|
||||
pos, pos + cur_skipgram_size)
|
||||
else:
|
||||
st_pos, end_pos = pos, pos + cur_skipgram_size
|
||||
else:
|
||||
# directly mask
|
||||
if self.mask_whole_word:
|
||||
st_pos, end_pos = _expand_whole_word(pos, pos + 1)
|
||||
else:
|
||||
st_pos, end_pos = pos, pos + 1
|
||||
|
||||
for mp in range(st_pos, end_pos):
|
||||
if (0 < mp <= max_cand_pos) and (mp not in special_pos):
|
||||
masked_pos.add(mp)
|
||||
else:
|
||||
break
|
||||
|
||||
masked_pos = list(masked_pos)
|
||||
if len(masked_pos) > n_pred:
|
||||
shuffle(masked_pos)
|
||||
masked_pos = masked_pos[:n_pred]
|
||||
|
||||
masked_tokens = [tokens[pos] for pos in masked_pos]
|
||||
for pos in masked_pos:
|
||||
if rand() < 0.8: # 80%
|
||||
tokens[pos] = '[MASK]'
|
||||
elif rand() < 0.5: # 10%
|
||||
tokens[pos] = get_random_word(self.vocab_words)
|
||||
# when n_pred < max_pred, we only calculate loss within n_pred
|
||||
masked_weights = [1]*len(masked_tokens)
|
||||
|
||||
# Token Indexing
|
||||
masked_ids = self.indexer(masked_tokens)
|
||||
# Token Indexing
|
||||
input_ids = self.indexer(tokens)
|
||||
|
||||
# Zero Padding
|
||||
n_pad = self.max_len - len(input_ids)
|
||||
input_ids.extend([0]*n_pad)
|
||||
segment_ids.extend([0]*n_pad)
|
||||
|
||||
if self.num_qkv > 1:
|
||||
mask_qkv = [0]*(len(tokens_a)+2) + [1] * (len(tokens_b)+1)
|
||||
mask_qkv.extend([0]*n_pad)
|
||||
else:
|
||||
mask_qkv = None
|
||||
|
||||
input_mask = torch.zeros(self.max_len, self.max_len, dtype=torch.long)
|
||||
if self.mode == "s2s":
|
||||
input_mask[:, :len(tokens_a)+2].fill_(1)
|
||||
second_st, second_end = len(
|
||||
tokens_a)+2, len(tokens_a)+len(tokens_b)+3
|
||||
input_mask[second_st:second_end, second_st:second_end].copy_(
|
||||
self._tril_matrix[:second_end-second_st, :second_end-second_st])
|
||||
else:
|
||||
st, end = 0, len(tokens_a) + len(tokens_b) + 3
|
||||
input_mask[st:end, st:end].copy_(self._tril_matrix[:end, :end])
|
||||
|
||||
# Zero Padding for masked target
|
||||
if self.max_pred > n_pred:
|
||||
n_pad = self.max_pred - n_pred
|
||||
if masked_ids is not None:
|
||||
masked_ids.extend([0]*n_pad)
|
||||
if masked_pos is not None:
|
||||
masked_pos.extend([0]*n_pad)
|
||||
if masked_weights is not None:
|
||||
masked_weights.extend([0]*n_pad)
|
||||
|
||||
oracle_pos = None
|
||||
oracle_weights = None
|
||||
oracle_labels = None
|
||||
if self.has_oracle:
|
||||
s_st, labls = instance[2:]
|
||||
oracle_pos = []
|
||||
oracle_labels = []
|
||||
for st, lb in zip(s_st, labls):
|
||||
st = st - num_truncated_a[0]
|
||||
if st > 0 and st < len(tokens_a):
|
||||
oracle_pos.append(st)
|
||||
oracle_labels.append(lb)
|
||||
oracle_pos = oracle_pos[:20]
|
||||
oracle_labels = oracle_labels[:20]
|
||||
oracle_weights = [1] * len(oracle_pos)
|
||||
if len(oracle_pos) < 20:
|
||||
x_pad = 20 - len(oracle_pos)
|
||||
oracle_pos.extend([0] * x_pad)
|
||||
oracle_labels.extend([0] * x_pad)
|
||||
oracle_weights.extend([0] * x_pad)
|
||||
|
||||
return (input_ids, segment_ids, input_mask, mask_qkv, masked_ids,
|
||||
masked_pos, masked_weights, -1, self.task_idx,
|
||||
oracle_pos, oracle_weights, oracle_labels)
|
||||
|
||||
return (input_ids, segment_ids, input_mask, mask_qkv, masked_ids, masked_pos, masked_weights, -1, self.task_idx)
|
||||
|
||||
|
||||
class Preprocess4Seq2seqDecoder(Pipeline):
|
||||
""" Pre-processing steps for pretraining transformer """
|
||||
|
||||
def __init__(self, vocab_words, indexer, max_len=512, max_tgt_length=128, new_segment_ids=False, mode="s2s", num_qkv=0, s2s_special_token=False, s2s_add_segment=False, s2s_share_segment=False, pos_shift=False):
|
||||
super().__init__()
|
||||
self.max_len = max_len
|
||||
self.vocab_words = vocab_words # vocabulary (sub)words
|
||||
self.indexer = indexer # function from token to token index
|
||||
self.max_len = max_len
|
||||
self._tril_matrix = torch.tril(torch.ones(
|
||||
(max_len, max_len), dtype=torch.long))
|
||||
self.new_segment_ids = new_segment_ids
|
||||
self.task_idx = 3 # relax projection layer for different tasks
|
||||
assert mode in ("s2s", "l2r")
|
||||
self.mode = mode
|
||||
self.max_tgt_length = max_tgt_length
|
||||
self.num_qkv = num_qkv
|
||||
self.s2s_special_token = s2s_special_token
|
||||
self.s2s_add_segment = s2s_add_segment
|
||||
self.s2s_share_segment = s2s_share_segment
|
||||
self.pos_shift = pos_shift
|
||||
|
||||
def __call__(self, instance):
|
||||
tokens_a, max_a_len = instance
|
||||
|
||||
# Add Special Tokens
|
||||
if self.s2s_special_token:
|
||||
padded_tokens_a = ['[S2S_CLS]'] + tokens_a + ['[S2S_SEP]']
|
||||
else:
|
||||
padded_tokens_a = ['[CLS]'] + tokens_a + ['[SEP]']
|
||||
assert len(padded_tokens_a) <= max_a_len + 2
|
||||
if max_a_len + 2 > len(padded_tokens_a):
|
||||
padded_tokens_a += ['[PAD]'] * \
|
||||
(max_a_len + 2 - len(padded_tokens_a))
|
||||
assert len(padded_tokens_a) == max_a_len + 2
|
||||
max_len_in_batch = min(self.max_tgt_length +
|
||||
max_a_len + 2, self.max_len)
|
||||
tokens = padded_tokens_a
|
||||
if self.new_segment_ids:
|
||||
if self.mode == "s2s":
|
||||
_enc_seg1 = 0 if self.s2s_share_segment else 4
|
||||
if self.s2s_add_segment:
|
||||
if self.s2s_share_segment:
|
||||
segment_ids = [
|
||||
0] + [1]*(len(padded_tokens_a)-1) + [5]*(max_len_in_batch - len(padded_tokens_a))
|
||||
else:
|
||||
segment_ids = [
|
||||
4] + [6]*(len(padded_tokens_a)-1) + [5]*(max_len_in_batch - len(padded_tokens_a))
|
||||
else:
|
||||
segment_ids = [4]*(len(padded_tokens_a)) + \
|
||||
[5]*(max_len_in_batch - len(padded_tokens_a))
|
||||
else:
|
||||
segment_ids = [2]*max_len_in_batch
|
||||
else:
|
||||
segment_ids = [0]*(len(padded_tokens_a)) \
|
||||
+ [1]*(max_len_in_batch - len(padded_tokens_a))
|
||||
|
||||
if self.num_qkv > 1:
|
||||
mask_qkv = [0]*(len(padded_tokens_a)) + [1] * \
|
||||
(max_len_in_batch - len(padded_tokens_a))
|
||||
else:
|
||||
mask_qkv = None
|
||||
|
||||
position_ids = []
|
||||
for i in range(len(tokens_a) + 2):
|
||||
position_ids.append(i)
|
||||
for i in range(len(tokens_a) + 2, max_a_len + 2):
|
||||
position_ids.append(0)
|
||||
for i in range(max_a_len + 2, max_len_in_batch):
|
||||
position_ids.append(i - (max_a_len + 2) + len(tokens_a) + 2)
|
||||
|
||||
# Token Indexing
|
||||
input_ids = self.indexer(tokens)
|
||||
|
||||
# Zero Padding
|
||||
input_mask = torch.zeros(
|
||||
max_len_in_batch, max_len_in_batch, dtype=torch.long)
|
||||
if self.mode == "s2s":
|
||||
input_mask[:, :len(tokens_a)+2].fill_(1)
|
||||
else:
|
||||
st, end = 0, len(tokens_a) + 2
|
||||
input_mask[st:end, st:end].copy_(
|
||||
self._tril_matrix[:end, :end])
|
||||
input_mask[end:, :len(tokens_a)+2].fill_(1)
|
||||
second_st, second_end = len(padded_tokens_a), max_len_in_batch
|
||||
|
||||
input_mask[second_st:second_end, second_st:second_end].copy_(
|
||||
self._tril_matrix[:second_end-second_st, :second_end-second_st])
|
||||
|
||||
return (input_ids, segment_ids, position_ids, input_mask, mask_qkv, self.task_idx)
|
||||
@@ -0,0 +1,644 @@
|
||||
from __future__ import print_function, unicode_literals, division
|
||||
|
||||
import os
|
||||
import re
|
||||
import codecs
|
||||
import platform
|
||||
|
||||
from subprocess import check_output
|
||||
from tempfile import mkdtemp
|
||||
from functools import partial
|
||||
|
||||
try:
|
||||
from configparser import ConfigParser
|
||||
except ImportError:
|
||||
from ConfigParser import ConfigParser
|
||||
|
||||
from pyrouge.utils import log
|
||||
from pyrouge.utils.file_utils import verify_dir
|
||||
|
||||
|
||||
REMAP = {"-lrb-": "(", "-rrb-": ")", "-lcb-": "{", "-rcb-": "}",
|
||||
"-lsb-": "[", "-rsb-": "]", "``": '"', "''": '"'}
|
||||
|
||||
|
||||
def clean(x):
|
||||
return re.sub(
|
||||
r"-lrb-|-rrb-|-lcb-|-rcb-|-lsb-|-rsb-|``|''",
|
||||
lambda m: REMAP.get(m.group()), x)
|
||||
|
||||
|
||||
class DirectoryProcessor:
|
||||
|
||||
@staticmethod
|
||||
def process(input_dir, output_dir, function):
|
||||
"""
|
||||
Apply function to all files in input_dir and save the resulting ouput
|
||||
files in output_dir.
|
||||
|
||||
"""
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
logger = log.get_global_console_logger()
|
||||
logger.info("Processing files in {}.".format(input_dir))
|
||||
input_file_names = os.listdir(input_dir)
|
||||
for input_file_name in input_file_names:
|
||||
input_file = os.path.join(input_dir, input_file_name)
|
||||
with codecs.open(input_file, "r", encoding="UTF-8") as f:
|
||||
input_string = f.read()
|
||||
output_string = function(input_string)
|
||||
output_file = os.path.join(output_dir, input_file_name)
|
||||
with codecs.open(output_file, "w", encoding="UTF-8") as f:
|
||||
f.write(clean(output_string.lower()))
|
||||
logger.info("Saved processed files to {}.".format(output_dir))
|
||||
|
||||
|
||||
class Rouge155(object):
|
||||
"""
|
||||
This is a wrapper for the ROUGE 1.5.5 summary evaluation package.
|
||||
This class is designed to simplify the evaluation process by:
|
||||
|
||||
1) Converting summaries into a format ROUGE understands.
|
||||
2) Generating the ROUGE configuration file automatically based
|
||||
on filename patterns.
|
||||
|
||||
This class can be used within Python like this:
|
||||
|
||||
rouge = Rouge155()
|
||||
rouge.system_dir = 'test/systems'
|
||||
rouge.model_dir = 'test/models'
|
||||
|
||||
# The system filename pattern should contain one group that
|
||||
# matches the document ID.
|
||||
rouge.system_filename_pattern = 'SL.P.10.R.11.SL062003-(\d+).html'
|
||||
|
||||
# The model filename pattern has '#ID#' as a placeholder for the
|
||||
# document ID. If there are multiple model summaries, pyrouge
|
||||
# will use the provided regex to automatically match them with
|
||||
# the corresponding system summary. Here, [A-Z] matches
|
||||
# multiple model summaries for a given #ID#.
|
||||
rouge.model_filename_pattern = 'SL.P.10.R.[A-Z].SL062003-#ID#.html'
|
||||
|
||||
rouge_output = rouge.evaluate()
|
||||
print(rouge_output)
|
||||
output_dict = rouge.output_to_dict(rouge_ouput)
|
||||
print(output_dict)
|
||||
-> {'rouge_1_f_score': 0.95652,
|
||||
'rouge_1_f_score_cb': 0.95652,
|
||||
'rouge_1_f_score_ce': 0.95652,
|
||||
'rouge_1_precision': 0.95652,
|
||||
[...]
|
||||
|
||||
|
||||
To evaluate multiple systems:
|
||||
|
||||
rouge = Rouge155()
|
||||
rouge.system_dir = '/PATH/TO/systems'
|
||||
rouge.model_dir = 'PATH/TO/models'
|
||||
for system_id in ['id1', 'id2', 'id3']:
|
||||
rouge.system_filename_pattern = \
|
||||
'SL.P/.10.R.{}.SL062003-(\d+).html'.format(system_id)
|
||||
rouge.model_filename_pattern = \
|
||||
'SL.P.10.R.[A-Z].SL062003-#ID#.html'
|
||||
rouge_output = rouge.evaluate(system_id)
|
||||
print(rouge_output)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, rouge_dir=None, rouge_args=None, temp_dir=None):
|
||||
"""
|
||||
Create a Rouge155 object.
|
||||
|
||||
rouge_dir: Directory containing Rouge-1.5.5.pl
|
||||
rouge_args: Arguments to pass through to ROUGE if you
|
||||
don't want to use the default pyrouge
|
||||
arguments.
|
||||
|
||||
"""
|
||||
self.temp_dir = temp_dir
|
||||
self.log = log.get_global_console_logger()
|
||||
self.__set_dir_properties()
|
||||
self._config_file = None
|
||||
self._settings_file = self.__get_config_path()
|
||||
self.__set_rouge_dir(rouge_dir)
|
||||
self.args = self.__clean_rouge_args(rouge_args)
|
||||
self._system_filename_pattern = None
|
||||
self._model_filename_pattern = None
|
||||
|
||||
def save_home_dir(self):
|
||||
config = ConfigParser()
|
||||
section = 'pyrouge settings'
|
||||
config.add_section(section)
|
||||
config.set(section, 'home_dir', self._home_dir)
|
||||
with open(self._settings_file, 'w') as f:
|
||||
config.write(f)
|
||||
self.log.info("Set ROUGE home directory to {}.".format(self._home_dir))
|
||||
|
||||
@property
|
||||
def settings_file(self):
|
||||
"""
|
||||
Path of the setttings file, which stores the ROUGE home dir.
|
||||
|
||||
"""
|
||||
return self._settings_file
|
||||
|
||||
@property
|
||||
def bin_path(self):
|
||||
"""
|
||||
The full path of the ROUGE binary (although it's technically
|
||||
a script), i.e. rouge_home_dir/ROUGE-1.5.5.pl
|
||||
|
||||
"""
|
||||
if self._bin_path is None:
|
||||
raise Exception(
|
||||
"ROUGE path not set. Please set the ROUGE home directory "
|
||||
"and ensure that ROUGE-1.5.5.pl exists in it.")
|
||||
return self._bin_path
|
||||
|
||||
@property
|
||||
def system_filename_pattern(self):
|
||||
"""
|
||||
The regular expression pattern for matching system summary
|
||||
filenames. The regex string.
|
||||
|
||||
E.g. "SL.P.10.R.11.SL062003-(\d+).html" will match the system
|
||||
filenames in the SPL2003/system folder of the ROUGE SPL example
|
||||
in the "sample-test" folder.
|
||||
|
||||
Currently, there is no support for multiple systems.
|
||||
|
||||
"""
|
||||
return self._system_filename_pattern
|
||||
|
||||
@system_filename_pattern.setter
|
||||
def system_filename_pattern(self, pattern):
|
||||
self._system_filename_pattern = pattern
|
||||
|
||||
@property
|
||||
def model_filename_pattern(self):
|
||||
"""
|
||||
The regular expression pattern for matching model summary
|
||||
filenames. The pattern needs to contain the string "#ID#",
|
||||
which is a placeholder for the document ID.
|
||||
|
||||
E.g. "SL.P.10.R.[A-Z].SL062003-#ID#.html" will match the model
|
||||
filenames in the SPL2003/system folder of the ROUGE SPL
|
||||
example in the "sample-test" folder.
|
||||
|
||||
"#ID#" is a placeholder for the document ID which has been
|
||||
matched by the "(\d+)" part of the system filename pattern.
|
||||
The different model summaries for a given document ID are
|
||||
matched by the "[A-Z]" part.
|
||||
|
||||
"""
|
||||
return self._model_filename_pattern
|
||||
|
||||
@model_filename_pattern.setter
|
||||
def model_filename_pattern(self, pattern):
|
||||
self._model_filename_pattern = pattern
|
||||
|
||||
@property
|
||||
def config_file(self):
|
||||
return self._config_file
|
||||
|
||||
@config_file.setter
|
||||
def config_file(self, path):
|
||||
config_dir, _ = os.path.split(path)
|
||||
verify_dir(config_dir, "configuration file")
|
||||
self._config_file = path
|
||||
|
||||
def split_sentences(self):
|
||||
"""
|
||||
ROUGE requires texts split into sentences. In case the texts
|
||||
are not already split, this method can be used.
|
||||
|
||||
"""
|
||||
from pyrouge.utils.sentence_splitter import PunktSentenceSplitter
|
||||
self.log.info("Splitting sentences.")
|
||||
ss = PunktSentenceSplitter()
|
||||
|
||||
def sent_split_to_string(s): return "\n".join(ss.split(s))
|
||||
process_func = partial(
|
||||
DirectoryProcessor.process, function=sent_split_to_string)
|
||||
self.__process_summaries(process_func)
|
||||
|
||||
@staticmethod
|
||||
def convert_summaries_to_rouge_format(input_dir, output_dir):
|
||||
"""
|
||||
Convert all files in input_dir into a format ROUGE understands
|
||||
and saves the files to output_dir. The input files are assumed
|
||||
to be plain text with one sentence per line.
|
||||
|
||||
input_dir: Path of directory containing the input files.
|
||||
output_dir: Path of directory in which the converted files
|
||||
will be saved.
|
||||
|
||||
"""
|
||||
DirectoryProcessor.process(
|
||||
input_dir, output_dir, Rouge155.convert_text_to_rouge_format)
|
||||
|
||||
@staticmethod
|
||||
def convert_text_to_rouge_format(text, title="dummy title"):
|
||||
"""
|
||||
Convert a text to a format ROUGE understands. The text is
|
||||
assumed to contain one sentence per line.
|
||||
|
||||
text: The text to convert, containg one sentence per line.
|
||||
title: Optional title for the text. The title will appear
|
||||
in the converted file, but doesn't seem to have
|
||||
any other relevance.
|
||||
|
||||
Returns: The converted text as string.
|
||||
|
||||
"""
|
||||
sentences = text.split("\n")
|
||||
sent_elems = [
|
||||
"<a name=\"{i}\">[{i}]</a> <a href=\"#{i}\" id={i}>"
|
||||
"{text}</a>".format(i=i, text=sent)
|
||||
for i, sent in enumerate(sentences, start=1)]
|
||||
html = """<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body bgcolor="white">
|
||||
{elems}
|
||||
</body>
|
||||
</html>""".format(title=title, elems="\n".join(sent_elems))
|
||||
|
||||
return html
|
||||
|
||||
@staticmethod
|
||||
def write_config_static(system_dir, system_filename_pattern,
|
||||
model_dir, model_filename_pattern,
|
||||
config_file_path, system_id=None):
|
||||
"""
|
||||
Write the ROUGE configuration file, which is basically a list
|
||||
of system summary files and their corresponding model summary
|
||||
files.
|
||||
|
||||
pyrouge uses regular expressions to automatically find the
|
||||
matching model summary files for a given system summary file
|
||||
(cf. docstrings for system_filename_pattern and
|
||||
model_filename_pattern).
|
||||
|
||||
system_dir: Path of directory containing
|
||||
system summaries.
|
||||
system_filename_pattern: Regex string for matching
|
||||
system summary filenames.
|
||||
model_dir: Path of directory containing
|
||||
model summaries.
|
||||
model_filename_pattern: Regex string for matching model
|
||||
summary filenames.
|
||||
config_file_path: Path of the configuration file.
|
||||
system_id: Optional system ID string which
|
||||
will appear in the ROUGE output.
|
||||
|
||||
"""
|
||||
system_filenames = [f for f in os.listdir(system_dir)]
|
||||
system_models_tuples = []
|
||||
|
||||
system_filename_pattern = re.compile(system_filename_pattern)
|
||||
for system_filename in sorted(system_filenames):
|
||||
match = system_filename_pattern.match(system_filename)
|
||||
if match:
|
||||
id = match.groups(0)[0]
|
||||
model_filenames = [model_filename_pattern.replace('#ID#', id)]
|
||||
# model_filenames = Rouge155.__get_model_filenames_for_id(
|
||||
# id, model_dir, model_filename_pattern)
|
||||
system_models_tuples.append(
|
||||
(system_filename, sorted(model_filenames)))
|
||||
if not system_models_tuples:
|
||||
raise Exception(
|
||||
"Did not find any files matching the pattern {} "
|
||||
"in the system summaries directory {}.".format(
|
||||
system_filename_pattern.pattern, system_dir))
|
||||
|
||||
with codecs.open(config_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write('<ROUGE-EVAL version="1.55">')
|
||||
for task_id, (system_filename, model_filenames) in enumerate(
|
||||
system_models_tuples, start=1):
|
||||
|
||||
eval_string = Rouge155.__get_eval_string(
|
||||
task_id, system_id,
|
||||
system_dir, system_filename,
|
||||
model_dir, model_filenames)
|
||||
f.write(eval_string)
|
||||
f.write("</ROUGE-EVAL>")
|
||||
|
||||
def write_config(self, config_file_path=None, system_id=None):
|
||||
"""
|
||||
Write the ROUGE configuration file, which is basically a list
|
||||
of system summary files and their matching model summary files.
|
||||
|
||||
This is a non-static version of write_config_file_static().
|
||||
|
||||
config_file_path: Path of the configuration file.
|
||||
system_id: Optional system ID string which will
|
||||
appear in the ROUGE output.
|
||||
|
||||
"""
|
||||
if not system_id:
|
||||
system_id = 1
|
||||
if (not config_file_path) or (not self._config_dir):
|
||||
self._config_dir = mkdtemp(dir=self.temp_dir)
|
||||
config_filename = "rouge_conf.xml"
|
||||
else:
|
||||
config_dir, config_filename = os.path.split(config_file_path)
|
||||
verify_dir(config_dir, "configuration file")
|
||||
self._config_file = os.path.join(self._config_dir, config_filename)
|
||||
Rouge155.write_config_static(
|
||||
self._system_dir, self._system_filename_pattern,
|
||||
self._model_dir, self._model_filename_pattern,
|
||||
self._config_file, system_id)
|
||||
self.log.info(
|
||||
"Written ROUGE configuration to {}".format(self._config_file))
|
||||
|
||||
def evaluate(self, system_id=1, rouge_args=None):
|
||||
"""
|
||||
Run ROUGE to evaluate the system summaries in system_dir against
|
||||
the model summaries in model_dir. The summaries are assumed to
|
||||
be in the one-sentence-per-line HTML format ROUGE understands.
|
||||
|
||||
system_id: Optional system ID which will be printed in
|
||||
ROUGE's output.
|
||||
|
||||
Returns: Rouge output as string.
|
||||
|
||||
"""
|
||||
self.write_config(system_id=system_id)
|
||||
options = self.__get_options(rouge_args)
|
||||
command = [self._bin_path] + options
|
||||
self.log.info(
|
||||
"Running ROUGE with command {}".format(" ".join(command)))
|
||||
rouge_output = check_output(command).decode("UTF-8")
|
||||
return rouge_output
|
||||
|
||||
def convert_and_evaluate(self, system_id=1,
|
||||
split_sentences=False, rouge_args=None):
|
||||
"""
|
||||
Convert plain text summaries to ROUGE format and run ROUGE to
|
||||
evaluate the system summaries in system_dir against the model
|
||||
summaries in model_dir. Optionally split texts into sentences
|
||||
in case they aren't already.
|
||||
|
||||
This is just a convenience method combining
|
||||
convert_summaries_to_rouge_format() and evaluate().
|
||||
|
||||
split_sentences: Optional argument specifying if
|
||||
sentences should be split.
|
||||
system_id: Optional system ID which will be printed
|
||||
in ROUGE's output.
|
||||
|
||||
Returns: ROUGE output as string.
|
||||
|
||||
"""
|
||||
if split_sentences:
|
||||
self.split_sentences()
|
||||
self.__write_summaries()
|
||||
rouge_output = self.evaluate(system_id, rouge_args)
|
||||
return rouge_output
|
||||
|
||||
def output_to_dict(self, output):
|
||||
"""
|
||||
Convert the ROUGE output into python dictionary for further
|
||||
processing.
|
||||
|
||||
"""
|
||||
# 0 ROUGE-1 Average_R: 0.02632 (95%-conf.int. 0.02632 - 0.02632)
|
||||
pattern = re.compile(
|
||||
r"(\d+) (ROUGE-\S+) (Average_\w): (\d.\d+) "
|
||||
r"\(95%-conf.int. (\d.\d+) - (\d.\d+)\)")
|
||||
results = {}
|
||||
for line in output.split("\n"):
|
||||
match = pattern.match(line)
|
||||
if match:
|
||||
sys_id, rouge_type, measure, result, conf_begin, conf_end = \
|
||||
match.groups()
|
||||
measure = {
|
||||
'Average_R': 'recall',
|
||||
'Average_P': 'precision',
|
||||
'Average_F': 'f_score'
|
||||
}[measure]
|
||||
rouge_type = rouge_type.lower().replace("-", '_')
|
||||
key = "{}_{}".format(rouge_type, measure)
|
||||
results[key] = float(result)
|
||||
results["{}_cb".format(key)] = float(conf_begin)
|
||||
results["{}_ce".format(key)] = float(conf_end)
|
||||
return results
|
||||
|
||||
###################################################################
|
||||
# Private methods
|
||||
|
||||
def __set_rouge_dir(self, home_dir=None):
|
||||
"""
|
||||
Verfify presence of ROUGE-1.5.5.pl and data folder, and set
|
||||
those paths.
|
||||
|
||||
"""
|
||||
if not home_dir:
|
||||
self._home_dir = self.__get_rouge_home_dir_from_settings()
|
||||
else:
|
||||
self._home_dir = home_dir
|
||||
self.save_home_dir()
|
||||
self._bin_path = os.path.join(self._home_dir, 'ROUGE-1.5.5.pl')
|
||||
self.data_dir = os.path.join(self._home_dir, 'data')
|
||||
if not os.path.exists(self._bin_path):
|
||||
raise Exception(
|
||||
"ROUGE binary not found at {}. Please set the "
|
||||
"correct path by running pyrouge_set_rouge_path "
|
||||
"/path/to/rouge/home.".format(self._bin_path))
|
||||
|
||||
def __get_rouge_home_dir_from_settings(self):
|
||||
config = ConfigParser()
|
||||
with open(self._settings_file) as f:
|
||||
if hasattr(config, "read_file"):
|
||||
config.read_file(f)
|
||||
else:
|
||||
# use deprecated python 2.x method
|
||||
config.readfp(f)
|
||||
rouge_home_dir = config.get('pyrouge settings', 'home_dir')
|
||||
return rouge_home_dir
|
||||
|
||||
@staticmethod
|
||||
def __get_eval_string(
|
||||
task_id, system_id,
|
||||
system_dir, system_filename,
|
||||
model_dir, model_filenames):
|
||||
"""
|
||||
ROUGE can evaluate several system summaries for a given text
|
||||
against several model summaries, i.e. there is an m-to-n
|
||||
relation between system and model summaries. The system
|
||||
summaries are listed in the <PEERS> tag and the model summaries
|
||||
in the <MODELS> tag. pyrouge currently only supports one system
|
||||
summary per text, i.e. it assumes a 1-to-n relation between
|
||||
system and model summaries.
|
||||
|
||||
"""
|
||||
peer_elems = "<P ID=\"{id}\">{name}</P>".format(
|
||||
id=system_id, name=system_filename)
|
||||
|
||||
model_elems = ["<M ID=\"{id}\">{name}</M>".format(
|
||||
id=chr(65 + i), name=name)
|
||||
for i, name in enumerate(model_filenames)]
|
||||
|
||||
model_elems = "\n\t\t\t".join(model_elems)
|
||||
eval_string = """
|
||||
<EVAL ID="{task_id}">
|
||||
<MODEL-ROOT>{model_root}</MODEL-ROOT>
|
||||
<PEER-ROOT>{peer_root}</PEER-ROOT>
|
||||
<INPUT-FORMAT TYPE="SEE">
|
||||
</INPUT-FORMAT>
|
||||
<PEERS>
|
||||
{peer_elems}
|
||||
</PEERS>
|
||||
<MODELS>
|
||||
{model_elems}
|
||||
</MODELS>
|
||||
</EVAL>
|
||||
""".format(
|
||||
task_id=task_id,
|
||||
model_root=model_dir, model_elems=model_elems,
|
||||
peer_root=system_dir, peer_elems=peer_elems)
|
||||
return eval_string
|
||||
|
||||
def __process_summaries(self, process_func):
|
||||
"""
|
||||
Helper method that applies process_func to the files in the
|
||||
system and model folders and saves the resulting files to new
|
||||
system and model folders.
|
||||
|
||||
"""
|
||||
temp_dir = mkdtemp(dir=self.temp_dir)
|
||||
new_system_dir = os.path.join(temp_dir, "system")
|
||||
os.mkdir(new_system_dir)
|
||||
new_model_dir = os.path.join(temp_dir, "model")
|
||||
os.mkdir(new_model_dir)
|
||||
self.log.info(
|
||||
"Processing summaries. Saving system files to {} and "
|
||||
"model files to {}.".format(new_system_dir, new_model_dir))
|
||||
process_func(self._system_dir, new_system_dir)
|
||||
process_func(self._model_dir, new_model_dir)
|
||||
self._system_dir = new_system_dir
|
||||
self._model_dir = new_model_dir
|
||||
|
||||
def __write_summaries(self):
|
||||
self.log.info("Writing summaries.")
|
||||
self.__process_summaries(self.convert_summaries_to_rouge_format)
|
||||
|
||||
@staticmethod
|
||||
def __get_model_filenames_for_id(id, model_dir, model_filenames_pattern):
|
||||
pattern = re.compile(model_filenames_pattern.replace('#ID#', id))
|
||||
model_filenames = [
|
||||
f for f in os.listdir(model_dir) if pattern.match(f)]
|
||||
if not model_filenames:
|
||||
raise Exception(
|
||||
"Could not find any model summaries for the system"
|
||||
" summary with ID {}. Specified model filename pattern was: "
|
||||
"{}".format(id, model_filenames_pattern))
|
||||
return model_filenames
|
||||
|
||||
def __get_options(self, rouge_args=None):
|
||||
"""
|
||||
Get supplied command line arguments for ROUGE or use default
|
||||
ones.
|
||||
|
||||
"""
|
||||
if self.args:
|
||||
options = self.args.split()
|
||||
elif rouge_args:
|
||||
options = rouge_args.split()
|
||||
else:
|
||||
options = [
|
||||
'-e', self._data_dir,
|
||||
'-c', 95,
|
||||
# '-2',
|
||||
# '-1',
|
||||
# '-U',
|
||||
'-m',
|
||||
# '-v',
|
||||
'-r', 1000,
|
||||
'-n', 2,
|
||||
# '-w', 1.2,
|
||||
'-a',
|
||||
]
|
||||
options = list(map(str, options))
|
||||
|
||||
options = self.__add_config_option(options)
|
||||
return options
|
||||
|
||||
def __create_dir_property(self, dir_name, docstring):
|
||||
"""
|
||||
Generate getter and setter for a directory property.
|
||||
|
||||
"""
|
||||
property_name = "{}_dir".format(dir_name)
|
||||
private_name = "_" + property_name
|
||||
setattr(self, private_name, None)
|
||||
|
||||
def fget(self):
|
||||
return getattr(self, private_name)
|
||||
|
||||
def fset(self, path):
|
||||
verify_dir(path, dir_name)
|
||||
setattr(self, private_name, path)
|
||||
|
||||
p = property(fget=fget, fset=fset, doc=docstring)
|
||||
setattr(self.__class__, property_name, p)
|
||||
|
||||
def __set_dir_properties(self):
|
||||
"""
|
||||
Automatically generate the properties for directories.
|
||||
|
||||
"""
|
||||
directories = [
|
||||
("home", "The ROUGE home directory."),
|
||||
("data", "The path of the ROUGE 'data' directory."),
|
||||
("system", "Path of the directory containing system summaries."),
|
||||
("model", "Path of the directory containing model summaries."),
|
||||
]
|
||||
for (dirname, docstring) in directories:
|
||||
self.__create_dir_property(dirname, docstring)
|
||||
|
||||
def __clean_rouge_args(self, rouge_args):
|
||||
"""
|
||||
Remove enclosing quotation marks, if any.
|
||||
|
||||
"""
|
||||
if not rouge_args:
|
||||
return
|
||||
quot_mark_pattern = re.compile('"(.+)"')
|
||||
match = quot_mark_pattern.match(rouge_args)
|
||||
if match:
|
||||
cleaned_args = match.group(1)
|
||||
return cleaned_args
|
||||
else:
|
||||
return rouge_args
|
||||
|
||||
def __add_config_option(self, options):
|
||||
return options + [self._config_file]
|
||||
|
||||
def __get_config_path(self):
|
||||
if platform.system() == "Windows":
|
||||
parent_dir = os.getenv("APPDATA")
|
||||
config_dir_name = "pyrouge"
|
||||
elif os.name == "posix":
|
||||
parent_dir = os.path.expanduser("~")
|
||||
config_dir_name = ".pyrouge"
|
||||
else:
|
||||
parent_dir = os.path.dirname(__file__)
|
||||
config_dir_name = ""
|
||||
config_dir = os.path.join(parent_dir, config_dir_name)
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir)
|
||||
return os.path.join(config_dir, 'settings.ini')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
from utils.argparsers import rouge_path_parser
|
||||
|
||||
parser = argparse.ArgumentParser(parents=[rouge_path_parser])
|
||||
args = parser.parse_args()
|
||||
|
||||
rouge = Rouge155(args.rouge_home)
|
||||
rouge.save_home_dir()
|
||||
@@ -0,0 +1,362 @@
|
||||
"""BERT finetuning runner."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import logging
|
||||
import glob
|
||||
import json
|
||||
import argparse
|
||||
import math
|
||||
import string
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from tqdm import tqdm, trange
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
# pip install py-rouge
|
||||
import rouge
|
||||
import time
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from pytorch_pretrained_bert.tokenization import BertTokenizer
|
||||
# pip install pyrouge
|
||||
from cnndm.bs_pyrouge import Rouge155
|
||||
|
||||
|
||||
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
|
||||
datefmt='%m/%d/%Y %H:%M:%S',
|
||||
level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# Required parameters
|
||||
parser.add_argument("--gold", type=str, help="Gold output file.")
|
||||
parser.add_argument("--pred", type=str, help="Input prediction file.")
|
||||
parser.add_argument("--split", type=str, default="",
|
||||
help="Data split (train/dev/test).")
|
||||
parser.add_argument("--save_best", action='store_true',
|
||||
help="Save best epoch.")
|
||||
parser.add_argument("--only_eval_best", action='store_true',
|
||||
help="Only evaluate best epoch.")
|
||||
parser.add_argument("--trunc_len", type=int, default=60,
|
||||
help="Truncate line by the maximum length.")
|
||||
parser.add_argument("--duplicate_rate", type=float, default=0.7,
|
||||
help="If the duplicat rate (compared with history) is large, we can discard the current sentence.")
|
||||
default_process_count = max(1, cpu_count() - 1)
|
||||
parser.add_argument("--processes", type=int, default=default_process_count,
|
||||
help="Number of processes to use (default %(default)s)")
|
||||
parser.add_argument("--perl", action='store_true',
|
||||
help="Using the perl script.")
|
||||
parser.add_argument('--lazy_eval', action='store_true',
|
||||
help="Skip evaluation if the .rouge file exists.")
|
||||
args = parser.parse_args()
|
||||
|
||||
SPECIAL_TOKEN = ["[UNK]", "[PAD]", "[CLS]", "[MASK]"]
|
||||
evaluator = rouge.Rouge(metrics=['rouge-n', 'rouge-l'], max_n=2,
|
||||
limit_length=False, apply_avg=True, weight_factor=1.2)
|
||||
|
||||
|
||||
def test_rouge(cand, ref):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
candidates = cand
|
||||
references = ref
|
||||
assert len(candidates) == len(references)
|
||||
|
||||
cnt = len(candidates)
|
||||
current_time = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime())
|
||||
tmp_dir = os.path.join(temp_dir, "rouge-tmp-{}".format(current_time))
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
os.mkdir(tmp_dir + "/candidate")
|
||||
os.mkdir(tmp_dir + "/reference")
|
||||
try:
|
||||
for i in range(cnt):
|
||||
if len(references[i]) < 1:
|
||||
continue
|
||||
with open(tmp_dir + "/candidate/cand.{}.txt".format(i), "w",
|
||||
encoding="utf-8") as f:
|
||||
f.write(candidates[i])
|
||||
with open(tmp_dir + "/reference/ref.{}.txt".format(i), "w",
|
||||
encoding="utf-8") as f:
|
||||
f.write(references[i])
|
||||
r = Rouge155(temp_dir=temp_dir)
|
||||
r.model_dir = tmp_dir + "/reference/"
|
||||
r.system_dir = tmp_dir + "/candidate/"
|
||||
r.model_filename_pattern = 'ref.#ID#.txt'
|
||||
r.system_filename_pattern = r'cand.(\d+).txt'
|
||||
rouge_results = r.convert_and_evaluate()
|
||||
print(rouge_results)
|
||||
results_dict = r.output_to_dict(rouge_results)
|
||||
finally:
|
||||
if os.path.isdir(tmp_dir):
|
||||
shutil.rmtree(tmp_dir)
|
||||
return results_dict
|
||||
|
||||
|
||||
def rouge_results_to_str(results_dict):
|
||||
return ">> ROUGE-F(1/2/l): {:.2f}/{:.2f}/{:.2f}\nROUGE-R(1/2/3/l): {:.2f}/{:.2f}/{:.2f}\n".format(
|
||||
results_dict["rouge_1_f_score"] * 100,
|
||||
results_dict["rouge_2_f_score"] * 100,
|
||||
results_dict["rouge_l_f_score"] * 100,
|
||||
results_dict["rouge_1_recall"] * 100,
|
||||
results_dict["rouge_2_recall"] * 100,
|
||||
results_dict["rouge_l_recall"] * 100
|
||||
)
|
||||
|
||||
|
||||
def count_tokens(tokens):
|
||||
counter = {}
|
||||
for t in tokens:
|
||||
if t in counter.keys():
|
||||
counter[t] += 1
|
||||
else:
|
||||
counter[t] = 1
|
||||
return counter
|
||||
|
||||
|
||||
def get_f1(text_a, text_b):
|
||||
tokens_a = text_a.lower().split()
|
||||
tokens_b = text_b.lower().split()
|
||||
if len(tokens_a) == 0 or len(tokens_b) == 0:
|
||||
return 1 if len(tokens_a) == len(tokens_b) else 0
|
||||
set_a = count_tokens(tokens_a)
|
||||
set_b = count_tokens(tokens_b)
|
||||
match = 0
|
||||
for token in set_a.keys():
|
||||
if token in set_b.keys():
|
||||
match += min(set_a[token], set_b[token])
|
||||
p = match / len(tokens_a)
|
||||
r = match / len(tokens_b)
|
||||
return 2.0 * p * r / (p + r + 1e-5)
|
||||
|
||||
|
||||
_tok_dict = {"(": "-LRB-", ")": "-RRB-",
|
||||
"[": "-LSB-", "]": "-RSB-",
|
||||
"{": "-LCB-", "}": "-RCB-"}
|
||||
|
||||
|
||||
def _is_digit(w):
|
||||
for ch in w:
|
||||
if not(ch.isdigit() or ch == ','):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def fix_tokenization(text):
|
||||
input_tokens = text.split()
|
||||
output_tokens = []
|
||||
has_left_quote = False
|
||||
has_left_single_quote = False
|
||||
|
||||
i = 0
|
||||
prev_dash = False
|
||||
while i < len(input_tokens):
|
||||
tok = input_tokens[i]
|
||||
flag_prev_dash = False
|
||||
if tok in _tok_dict.keys():
|
||||
output_tokens.append(_tok_dict[tok])
|
||||
i += 1
|
||||
elif tok == "\"":
|
||||
if has_left_quote:
|
||||
output_tokens.append("''")
|
||||
else:
|
||||
output_tokens.append("``")
|
||||
has_left_quote = not has_left_quote
|
||||
i += 1
|
||||
elif tok == "'" and len(output_tokens) > 0 and output_tokens[-1].endswith("n") and i < len(input_tokens) - 1 and input_tokens[i + 1] == "t":
|
||||
output_tokens[-1] = output_tokens[-1][:-1]
|
||||
output_tokens.append("n't")
|
||||
i += 2
|
||||
elif tok == "'" and i < len(input_tokens) - 1 and input_tokens[i + 1] in ("s", "d", "ll"):
|
||||
output_tokens.append("'"+input_tokens[i + 1])
|
||||
i += 2
|
||||
elif tok == "'":
|
||||
if has_left_single_quote:
|
||||
output_tokens.append("'")
|
||||
else:
|
||||
output_tokens.append("`")
|
||||
has_left_single_quote = not has_left_single_quote
|
||||
i += 1
|
||||
elif tok == "." and i < len(input_tokens) - 2 and input_tokens[i + 1] == "." and input_tokens[i + 2] == ".":
|
||||
output_tokens.append("...")
|
||||
i += 3
|
||||
elif tok == "," and len(output_tokens) > 0 and _is_digit(output_tokens[-1]) and i < len(input_tokens) - 1 and _is_digit(input_tokens[i + 1]):
|
||||
# $ 3 , 000 -> $ 3,000
|
||||
output_tokens[-1] += ','+input_tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "." and len(output_tokens) > 0 and output_tokens[-1].isdigit() and i < len(input_tokens) - 1 and input_tokens[i + 1].isdigit():
|
||||
# 3 . 03 -> $ 3.03
|
||||
output_tokens[-1] += '.'+input_tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "." and len(output_tokens) > 0 and len(output_tokens[-1]) == 1 and output_tokens[-1].isupper() and i < len(input_tokens) - 2 and len(input_tokens[i + 1]) == 1 and input_tokens[i + 1].isupper() and input_tokens[i + 2] == '.':
|
||||
# U . N . -> U.N.
|
||||
k = i+3
|
||||
while k+2 < len(input_tokens):
|
||||
if len(input_tokens[k + 1]) == 1 and input_tokens[k + 1].isupper() and input_tokens[k + 2] == '.':
|
||||
k += 2
|
||||
else:
|
||||
break
|
||||
output_tokens[-1] += ''.join(input_tokens[i:k])
|
||||
i += 2
|
||||
elif tok == "-":
|
||||
if i < len(input_tokens) - 1 and input_tokens[i + 1] == "-":
|
||||
output_tokens.append("--")
|
||||
i += 2
|
||||
elif i == len(input_tokens) - 1 or i == 0:
|
||||
output_tokens.append("-")
|
||||
i += 1
|
||||
elif output_tokens[-1] not in string.punctuation and input_tokens[i + 1][0] not in string.punctuation:
|
||||
output_tokens[-1] += "-"
|
||||
i += 1
|
||||
flag_prev_dash = True
|
||||
else:
|
||||
output_tokens.append("-")
|
||||
i += 1
|
||||
elif prev_dash and len(output_tokens) > 0 and tok[0] not in string.punctuation:
|
||||
output_tokens[-1] += tok
|
||||
i += 1
|
||||
else:
|
||||
output_tokens.append(tok)
|
||||
i += 1
|
||||
prev_dash = flag_prev_dash
|
||||
return " ".join(output_tokens)
|
||||
|
||||
|
||||
def remove_duplicate(l_list, duplicate_rate):
|
||||
tk_list = [l.lower().split() for l in l_list]
|
||||
r_list = []
|
||||
history_set = set()
|
||||
for i, w_list in enumerate(tk_list):
|
||||
w_set = set(w_list)
|
||||
if len(w_set & history_set)/len(w_set) <= duplicate_rate:
|
||||
r_list.append(l_list[i])
|
||||
history_set |= w_set
|
||||
return r_list
|
||||
|
||||
|
||||
def process_eval(eval_fn):
|
||||
gold_list = []
|
||||
with open(args.gold, "r", encoding="utf-8") as f_in:
|
||||
for l in f_in:
|
||||
line = l.strip().replace(" <S_SEP> ", '\n')
|
||||
gold_list.append(line)
|
||||
|
||||
pred_list = []
|
||||
with open(eval_fn, "r", encoding="utf-8") as f_in:
|
||||
for l in f_in:
|
||||
buf = []
|
||||
for sentence in l.strip().split("[X_SEP]"):
|
||||
sentence = fix_tokenization(sentence)
|
||||
if any(get_f1(sentence, s) > 1.0 for s in buf):
|
||||
continue
|
||||
s_len = len(sentence.split())
|
||||
if s_len <= 4:
|
||||
continue
|
||||
buf.append(sentence)
|
||||
if args.duplicate_rate and args.duplicate_rate < 1:
|
||||
buf = remove_duplicate(buf, args.duplicate_rate)
|
||||
if args.trunc_len:
|
||||
num_left = args.trunc_len
|
||||
trunc_list = []
|
||||
for bit in buf:
|
||||
tk_list = bit.split()
|
||||
n = min(len(tk_list), num_left)
|
||||
trunc_list.append(' '.join(tk_list[:n]))
|
||||
num_left -= n
|
||||
if num_left <= 0:
|
||||
break
|
||||
else:
|
||||
trunc_list = buf
|
||||
line = "\n".join(trunc_list)
|
||||
pred_list.append(line)
|
||||
with open(eval_fn+'.post', 'w', encoding='utf-8') as f_out:
|
||||
for l in pred_list:
|
||||
f_out.write(l.replace('\n', ' [X_SEP] ').strip())
|
||||
f_out.write('\n')
|
||||
# rouge scores
|
||||
if len(pred_list) < len(gold_list):
|
||||
# evaluate subset
|
||||
gold_list = gold_list[:len(pred_list)]
|
||||
assert len(pred_list) == len(gold_list)
|
||||
if args.perl:
|
||||
scores = test_rouge(pred_list, gold_list)
|
||||
else:
|
||||
scores = evaluator.get_scores(pred_list, [[it] for it in gold_list])
|
||||
return eval_fn, scores
|
||||
|
||||
|
||||
def main():
|
||||
if args.perl:
|
||||
eval_fn_list = list(glob.glob(args.pred))
|
||||
else:
|
||||
eval_fn_list = [eval_fn for eval_fn in glob.glob(args.pred) if not(
|
||||
args.lazy_eval and Path(eval_fn+".rouge").exists())]
|
||||
eval_fn_list = list(filter(lambda fn: not(fn.endswith(
|
||||
'.post') or fn.endswith('.rouge')), eval_fn_list))
|
||||
|
||||
if args.only_eval_best:
|
||||
best_epoch_dict = {}
|
||||
for dir_path in set(Path(fn).parent for fn in eval_fn_list):
|
||||
fn_save = os.path.join(dir_path, 'save_best.dev')
|
||||
if Path(fn_save).exists():
|
||||
with open(fn_save, 'r') as f_in:
|
||||
__, o_name, __ = f_in.read().strip().split('\n')
|
||||
epoch = o_name.split('.')[1]
|
||||
best_epoch_dict[dir_path] = epoch
|
||||
new_eval_fn_list = []
|
||||
for fn in eval_fn_list:
|
||||
dir_path = Path(fn).parent
|
||||
if dir_path in best_epoch_dict:
|
||||
if Path(fn).name.split('.')[1] == best_epoch_dict[dir_path]:
|
||||
new_eval_fn_list.append(fn)
|
||||
eval_fn_list = new_eval_fn_list
|
||||
|
||||
logger.info("***** Evaluation: %s *****", ','.join(eval_fn_list))
|
||||
num_pool = min(args.processes, len(eval_fn_list))
|
||||
p = Pool(num_pool)
|
||||
r_list = p.imap_unordered(process_eval, eval_fn_list)
|
||||
r_list = sorted([(fn, scores)
|
||||
for fn, scores in r_list], key=lambda x: x[0])
|
||||
rg2_dict = {}
|
||||
for fn, scores in r_list:
|
||||
print(fn)
|
||||
if args.perl:
|
||||
print(rouge_results_to_str(scores))
|
||||
else:
|
||||
rg2_dict[fn] = scores['rouge-2']['f']
|
||||
print(
|
||||
"ROUGE-1: {}\tROUGE-2: {}\n".format(scores['rouge-1']['f'], scores['rouge-2']['f']))
|
||||
with open(fn+".rouge", 'w') as f_out:
|
||||
f_out.write(json.dumps(
|
||||
{'rg1': scores['rouge-1']['f'], 'rg2': scores['rouge-2']['f']}))
|
||||
p.close()
|
||||
p.join()
|
||||
|
||||
if args.save_best:
|
||||
# find best results
|
||||
group_dict = {}
|
||||
for k, v in rg2_dict.items():
|
||||
d_name, o_name = Path(k).parent, Path(k).name
|
||||
if (d_name not in group_dict) or (v > group_dict[d_name][1]):
|
||||
group_dict[d_name] = (o_name, v)
|
||||
# compare and save the best result
|
||||
for k, v in group_dict.items():
|
||||
fn = os.path.join(k, 'save_best.'+args.split)
|
||||
o_name_s, rst_s = v
|
||||
should_save = True
|
||||
if Path(fn).exists():
|
||||
with open(fn, 'r') as f_in:
|
||||
rst_f = float(f_in.read().strip().split('\n')[-1])
|
||||
if rst_s <= rst_f:
|
||||
should_save = False
|
||||
if should_save:
|
||||
with open(fn, 'w') as f_out:
|
||||
f_out.write('{0}\n{1}\n{2}\n'.format(k, o_name_s, rst_s))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,644 @@
|
||||
from __future__ import print_function, unicode_literals, division
|
||||
|
||||
import os
|
||||
import re
|
||||
import codecs
|
||||
import platform
|
||||
|
||||
from subprocess import check_output
|
||||
from tempfile import mkdtemp
|
||||
from functools import partial
|
||||
|
||||
try:
|
||||
from configparser import ConfigParser
|
||||
except ImportError:
|
||||
from ConfigParser import ConfigParser
|
||||
|
||||
from pyrouge.utils import log
|
||||
from pyrouge.utils.file_utils import verify_dir
|
||||
|
||||
|
||||
REMAP = {"-lrb-": "(", "-rrb-": ")", "-lcb-": "{", "-rcb-": "}",
|
||||
"-lsb-": "[", "-rsb-": "]", "``": '"', "''": '"'}
|
||||
|
||||
|
||||
def clean(x):
|
||||
return re.sub(
|
||||
r"-lrb-|-rrb-|-lcb-|-rcb-|-lsb-|-rsb-|``|''",
|
||||
lambda m: REMAP.get(m.group()), x)
|
||||
|
||||
|
||||
class DirectoryProcessor:
|
||||
|
||||
@staticmethod
|
||||
def process(input_dir, output_dir, function):
|
||||
"""
|
||||
Apply function to all files in input_dir and save the resulting ouput
|
||||
files in output_dir.
|
||||
|
||||
"""
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
logger = log.get_global_console_logger()
|
||||
logger.info("Processing files in {}.".format(input_dir))
|
||||
input_file_names = os.listdir(input_dir)
|
||||
for input_file_name in input_file_names:
|
||||
input_file = os.path.join(input_dir, input_file_name)
|
||||
with codecs.open(input_file, "r", encoding="UTF-8") as f:
|
||||
input_string = f.read()
|
||||
output_string = function(input_string)
|
||||
output_file = os.path.join(output_dir, input_file_name)
|
||||
with codecs.open(output_file, "w", encoding="UTF-8") as f:
|
||||
f.write(clean(output_string.lower()))
|
||||
logger.info("Saved processed files to {}.".format(output_dir))
|
||||
|
||||
|
||||
class Rouge155(object):
|
||||
"""
|
||||
This is a wrapper for the ROUGE 1.5.5 summary evaluation package.
|
||||
This class is designed to simplify the evaluation process by:
|
||||
|
||||
1) Converting summaries into a format ROUGE understands.
|
||||
2) Generating the ROUGE configuration file automatically based
|
||||
on filename patterns.
|
||||
|
||||
This class can be used within Python like this:
|
||||
|
||||
rouge = Rouge155()
|
||||
rouge.system_dir = 'test/systems'
|
||||
rouge.model_dir = 'test/models'
|
||||
|
||||
# The system filename pattern should contain one group that
|
||||
# matches the document ID.
|
||||
rouge.system_filename_pattern = 'SL.P.10.R.11.SL062003-(\d+).html'
|
||||
|
||||
# The model filename pattern has '#ID#' as a placeholder for the
|
||||
# document ID. If there are multiple model summaries, pyrouge
|
||||
# will use the provided regex to automatically match them with
|
||||
# the corresponding system summary. Here, [A-Z] matches
|
||||
# multiple model summaries for a given #ID#.
|
||||
rouge.model_filename_pattern = 'SL.P.10.R.[A-Z].SL062003-#ID#.html'
|
||||
|
||||
rouge_output = rouge.evaluate()
|
||||
print(rouge_output)
|
||||
output_dict = rouge.output_to_dict(rouge_ouput)
|
||||
print(output_dict)
|
||||
-> {'rouge_1_f_score': 0.95652,
|
||||
'rouge_1_f_score_cb': 0.95652,
|
||||
'rouge_1_f_score_ce': 0.95652,
|
||||
'rouge_1_precision': 0.95652,
|
||||
[...]
|
||||
|
||||
|
||||
To evaluate multiple systems:
|
||||
|
||||
rouge = Rouge155()
|
||||
rouge.system_dir = '/PATH/TO/systems'
|
||||
rouge.model_dir = 'PATH/TO/models'
|
||||
for system_id in ['id1', 'id2', 'id3']:
|
||||
rouge.system_filename_pattern = \
|
||||
'SL.P/.10.R.{}.SL062003-(\d+).html'.format(system_id)
|
||||
rouge.model_filename_pattern = \
|
||||
'SL.P.10.R.[A-Z].SL062003-#ID#.html'
|
||||
rouge_output = rouge.evaluate(system_id)
|
||||
print(rouge_output)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, rouge_dir=None, rouge_args=None, temp_dir=None):
|
||||
"""
|
||||
Create a Rouge155 object.
|
||||
|
||||
rouge_dir: Directory containing Rouge-1.5.5.pl
|
||||
rouge_args: Arguments to pass through to ROUGE if you
|
||||
don't want to use the default pyrouge
|
||||
arguments.
|
||||
|
||||
"""
|
||||
self.temp_dir = temp_dir
|
||||
self.log = log.get_global_console_logger()
|
||||
self.__set_dir_properties()
|
||||
self._config_file = None
|
||||
self._settings_file = self.__get_config_path()
|
||||
self.__set_rouge_dir(rouge_dir)
|
||||
self.args = self.__clean_rouge_args(rouge_args)
|
||||
self._system_filename_pattern = None
|
||||
self._model_filename_pattern = None
|
||||
|
||||
def save_home_dir(self):
|
||||
config = ConfigParser()
|
||||
section = 'pyrouge settings'
|
||||
config.add_section(section)
|
||||
config.set(section, 'home_dir', self._home_dir)
|
||||
with open(self._settings_file, 'w') as f:
|
||||
config.write(f)
|
||||
self.log.info("Set ROUGE home directory to {}.".format(self._home_dir))
|
||||
|
||||
@property
|
||||
def settings_file(self):
|
||||
"""
|
||||
Path of the setttings file, which stores the ROUGE home dir.
|
||||
|
||||
"""
|
||||
return self._settings_file
|
||||
|
||||
@property
|
||||
def bin_path(self):
|
||||
"""
|
||||
The full path of the ROUGE binary (although it's technically
|
||||
a script), i.e. rouge_home_dir/ROUGE-1.5.5.pl
|
||||
|
||||
"""
|
||||
if self._bin_path is None:
|
||||
raise Exception(
|
||||
"ROUGE path not set. Please set the ROUGE home directory "
|
||||
"and ensure that ROUGE-1.5.5.pl exists in it.")
|
||||
return self._bin_path
|
||||
|
||||
@property
|
||||
def system_filename_pattern(self):
|
||||
"""
|
||||
The regular expression pattern for matching system summary
|
||||
filenames. The regex string.
|
||||
|
||||
E.g. "SL.P.10.R.11.SL062003-(\d+).html" will match the system
|
||||
filenames in the SPL2003/system folder of the ROUGE SPL example
|
||||
in the "sample-test" folder.
|
||||
|
||||
Currently, there is no support for multiple systems.
|
||||
|
||||
"""
|
||||
return self._system_filename_pattern
|
||||
|
||||
@system_filename_pattern.setter
|
||||
def system_filename_pattern(self, pattern):
|
||||
self._system_filename_pattern = pattern
|
||||
|
||||
@property
|
||||
def model_filename_pattern(self):
|
||||
"""
|
||||
The regular expression pattern for matching model summary
|
||||
filenames. The pattern needs to contain the string "#ID#",
|
||||
which is a placeholder for the document ID.
|
||||
|
||||
E.g. "SL.P.10.R.[A-Z].SL062003-#ID#.html" will match the model
|
||||
filenames in the SPL2003/system folder of the ROUGE SPL
|
||||
example in the "sample-test" folder.
|
||||
|
||||
"#ID#" is a placeholder for the document ID which has been
|
||||
matched by the "(\d+)" part of the system filename pattern.
|
||||
The different model summaries for a given document ID are
|
||||
matched by the "[A-Z]" part.
|
||||
|
||||
"""
|
||||
return self._model_filename_pattern
|
||||
|
||||
@model_filename_pattern.setter
|
||||
def model_filename_pattern(self, pattern):
|
||||
self._model_filename_pattern = pattern
|
||||
|
||||
@property
|
||||
def config_file(self):
|
||||
return self._config_file
|
||||
|
||||
@config_file.setter
|
||||
def config_file(self, path):
|
||||
config_dir, _ = os.path.split(path)
|
||||
verify_dir(config_dir, "configuration file")
|
||||
self._config_file = path
|
||||
|
||||
def split_sentences(self):
|
||||
"""
|
||||
ROUGE requires texts split into sentences. In case the texts
|
||||
are not already split, this method can be used.
|
||||
|
||||
"""
|
||||
from pyrouge.utils.sentence_splitter import PunktSentenceSplitter
|
||||
self.log.info("Splitting sentences.")
|
||||
ss = PunktSentenceSplitter()
|
||||
|
||||
def sent_split_to_string(s): return "\n".join(ss.split(s))
|
||||
process_func = partial(
|
||||
DirectoryProcessor.process, function=sent_split_to_string)
|
||||
self.__process_summaries(process_func)
|
||||
|
||||
@staticmethod
|
||||
def convert_summaries_to_rouge_format(input_dir, output_dir):
|
||||
"""
|
||||
Convert all files in input_dir into a format ROUGE understands
|
||||
and saves the files to output_dir. The input files are assumed
|
||||
to be plain text with one sentence per line.
|
||||
|
||||
input_dir: Path of directory containing the input files.
|
||||
output_dir: Path of directory in which the converted files
|
||||
will be saved.
|
||||
|
||||
"""
|
||||
DirectoryProcessor.process(
|
||||
input_dir, output_dir, Rouge155.convert_text_to_rouge_format)
|
||||
|
||||
@staticmethod
|
||||
def convert_text_to_rouge_format(text, title="dummy title"):
|
||||
"""
|
||||
Convert a text to a format ROUGE understands. The text is
|
||||
assumed to contain one sentence per line.
|
||||
|
||||
text: The text to convert, containg one sentence per line.
|
||||
title: Optional title for the text. The title will appear
|
||||
in the converted file, but doesn't seem to have
|
||||
any other relevance.
|
||||
|
||||
Returns: The converted text as string.
|
||||
|
||||
"""
|
||||
sentences = text.split("\n")
|
||||
sent_elems = [
|
||||
"<a name=\"{i}\">[{i}]</a> <a href=\"#{i}\" id={i}>"
|
||||
"{text}</a>".format(i=i, text=sent)
|
||||
for i, sent in enumerate(sentences, start=1)]
|
||||
html = """<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body bgcolor="white">
|
||||
{elems}
|
||||
</body>
|
||||
</html>""".format(title=title, elems="\n".join(sent_elems))
|
||||
|
||||
return html
|
||||
|
||||
@staticmethod
|
||||
def write_config_static(system_dir, system_filename_pattern,
|
||||
model_dir, model_filename_pattern,
|
||||
config_file_path, system_id=None):
|
||||
"""
|
||||
Write the ROUGE configuration file, which is basically a list
|
||||
of system summary files and their corresponding model summary
|
||||
files.
|
||||
|
||||
pyrouge uses regular expressions to automatically find the
|
||||
matching model summary files for a given system summary file
|
||||
(cf. docstrings for system_filename_pattern and
|
||||
model_filename_pattern).
|
||||
|
||||
system_dir: Path of directory containing
|
||||
system summaries.
|
||||
system_filename_pattern: Regex string for matching
|
||||
system summary filenames.
|
||||
model_dir: Path of directory containing
|
||||
model summaries.
|
||||
model_filename_pattern: Regex string for matching model
|
||||
summary filenames.
|
||||
config_file_path: Path of the configuration file.
|
||||
system_id: Optional system ID string which
|
||||
will appear in the ROUGE output.
|
||||
|
||||
"""
|
||||
system_filenames = [f for f in os.listdir(system_dir)]
|
||||
system_models_tuples = []
|
||||
|
||||
system_filename_pattern = re.compile(system_filename_pattern)
|
||||
for system_filename in sorted(system_filenames):
|
||||
match = system_filename_pattern.match(system_filename)
|
||||
if match:
|
||||
id = match.groups(0)[0]
|
||||
model_filenames = [model_filename_pattern.replace('#ID#', id)]
|
||||
# model_filenames = Rouge155.__get_model_filenames_for_id(
|
||||
# id, model_dir, model_filename_pattern)
|
||||
system_models_tuples.append(
|
||||
(system_filename, sorted(model_filenames)))
|
||||
if not system_models_tuples:
|
||||
raise Exception(
|
||||
"Did not find any files matching the pattern {} "
|
||||
"in the system summaries directory {}.".format(
|
||||
system_filename_pattern.pattern, system_dir))
|
||||
|
||||
with codecs.open(config_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write('<ROUGE-EVAL version="1.55">')
|
||||
for task_id, (system_filename, model_filenames) in enumerate(
|
||||
system_models_tuples, start=1):
|
||||
|
||||
eval_string = Rouge155.__get_eval_string(
|
||||
task_id, system_id,
|
||||
system_dir, system_filename,
|
||||
model_dir, model_filenames)
|
||||
f.write(eval_string)
|
||||
f.write("</ROUGE-EVAL>")
|
||||
|
||||
def write_config(self, config_file_path=None, system_id=None):
|
||||
"""
|
||||
Write the ROUGE configuration file, which is basically a list
|
||||
of system summary files and their matching model summary files.
|
||||
|
||||
This is a non-static version of write_config_file_static().
|
||||
|
||||
config_file_path: Path of the configuration file.
|
||||
system_id: Optional system ID string which will
|
||||
appear in the ROUGE output.
|
||||
|
||||
"""
|
||||
if not system_id:
|
||||
system_id = 1
|
||||
if (not config_file_path) or (not self._config_dir):
|
||||
self._config_dir = mkdtemp(dir=self.temp_dir)
|
||||
config_filename = "rouge_conf.xml"
|
||||
else:
|
||||
config_dir, config_filename = os.path.split(config_file_path)
|
||||
verify_dir(config_dir, "configuration file")
|
||||
self._config_file = os.path.join(self._config_dir, config_filename)
|
||||
Rouge155.write_config_static(
|
||||
self._system_dir, self._system_filename_pattern,
|
||||
self._model_dir, self._model_filename_pattern,
|
||||
self._config_file, system_id)
|
||||
self.log.info(
|
||||
"Written ROUGE configuration to {}".format(self._config_file))
|
||||
|
||||
def evaluate(self, system_id=1, rouge_args=None):
|
||||
"""
|
||||
Run ROUGE to evaluate the system summaries in system_dir against
|
||||
the model summaries in model_dir. The summaries are assumed to
|
||||
be in the one-sentence-per-line HTML format ROUGE understands.
|
||||
|
||||
system_id: Optional system ID which will be printed in
|
||||
ROUGE's output.
|
||||
|
||||
Returns: Rouge output as string.
|
||||
|
||||
"""
|
||||
self.write_config(system_id=system_id)
|
||||
options = self.__get_options(rouge_args)
|
||||
command = [self._bin_path] + options
|
||||
self.log.info(
|
||||
"Running ROUGE with command {}".format(" ".join(command)))
|
||||
rouge_output = check_output(command).decode("UTF-8")
|
||||
return rouge_output
|
||||
|
||||
def convert_and_evaluate(self, system_id=1,
|
||||
split_sentences=False, rouge_args=None):
|
||||
"""
|
||||
Convert plain text summaries to ROUGE format and run ROUGE to
|
||||
evaluate the system summaries in system_dir against the model
|
||||
summaries in model_dir. Optionally split texts into sentences
|
||||
in case they aren't already.
|
||||
|
||||
This is just a convenience method combining
|
||||
convert_summaries_to_rouge_format() and evaluate().
|
||||
|
||||
split_sentences: Optional argument specifying if
|
||||
sentences should be split.
|
||||
system_id: Optional system ID which will be printed
|
||||
in ROUGE's output.
|
||||
|
||||
Returns: ROUGE output as string.
|
||||
|
||||
"""
|
||||
if split_sentences:
|
||||
self.split_sentences()
|
||||
self.__write_summaries()
|
||||
rouge_output = self.evaluate(system_id, rouge_args)
|
||||
return rouge_output
|
||||
|
||||
def output_to_dict(self, output):
|
||||
"""
|
||||
Convert the ROUGE output into python dictionary for further
|
||||
processing.
|
||||
|
||||
"""
|
||||
# 0 ROUGE-1 Average_R: 0.02632 (95%-conf.int. 0.02632 - 0.02632)
|
||||
pattern = re.compile(
|
||||
r"(\d+) (ROUGE-\S+) (Average_\w): (\d.\d+) "
|
||||
r"\(95%-conf.int. (\d.\d+) - (\d.\d+)\)")
|
||||
results = {}
|
||||
for line in output.split("\n"):
|
||||
match = pattern.match(line)
|
||||
if match:
|
||||
sys_id, rouge_type, measure, result, conf_begin, conf_end = \
|
||||
match.groups()
|
||||
measure = {
|
||||
'Average_R': 'recall',
|
||||
'Average_P': 'precision',
|
||||
'Average_F': 'f_score'
|
||||
}[measure]
|
||||
rouge_type = rouge_type.lower().replace("-", '_')
|
||||
key = "{}_{}".format(rouge_type, measure)
|
||||
results[key] = float(result)
|
||||
results["{}_cb".format(key)] = float(conf_begin)
|
||||
results["{}_ce".format(key)] = float(conf_end)
|
||||
return results
|
||||
|
||||
###################################################################
|
||||
# Private methods
|
||||
|
||||
def __set_rouge_dir(self, home_dir=None):
|
||||
"""
|
||||
Verfify presence of ROUGE-1.5.5.pl and data folder, and set
|
||||
those paths.
|
||||
|
||||
"""
|
||||
if not home_dir:
|
||||
self._home_dir = self.__get_rouge_home_dir_from_settings()
|
||||
else:
|
||||
self._home_dir = home_dir
|
||||
self.save_home_dir()
|
||||
self._bin_path = os.path.join(self._home_dir, 'ROUGE-1.5.5.pl')
|
||||
self.data_dir = os.path.join(self._home_dir, 'data')
|
||||
if not os.path.exists(self._bin_path):
|
||||
raise Exception(
|
||||
"ROUGE binary not found at {}. Please set the "
|
||||
"correct path by running pyrouge_set_rouge_path "
|
||||
"/path/to/rouge/home.".format(self._bin_path))
|
||||
|
||||
def __get_rouge_home_dir_from_settings(self):
|
||||
config = ConfigParser()
|
||||
with open(self._settings_file) as f:
|
||||
if hasattr(config, "read_file"):
|
||||
config.read_file(f)
|
||||
else:
|
||||
# use deprecated python 2.x method
|
||||
config.readfp(f)
|
||||
rouge_home_dir = config.get('pyrouge settings', 'home_dir')
|
||||
return rouge_home_dir
|
||||
|
||||
@staticmethod
|
||||
def __get_eval_string(
|
||||
task_id, system_id,
|
||||
system_dir, system_filename,
|
||||
model_dir, model_filenames):
|
||||
"""
|
||||
ROUGE can evaluate several system summaries for a given text
|
||||
against several model summaries, i.e. there is an m-to-n
|
||||
relation between system and model summaries. The system
|
||||
summaries are listed in the <PEERS> tag and the model summaries
|
||||
in the <MODELS> tag. pyrouge currently only supports one system
|
||||
summary per text, i.e. it assumes a 1-to-n relation between
|
||||
system and model summaries.
|
||||
|
||||
"""
|
||||
peer_elems = "<P ID=\"{id}\">{name}</P>".format(
|
||||
id=system_id, name=system_filename)
|
||||
|
||||
model_elems = ["<M ID=\"{id}\">{name}</M>".format(
|
||||
id=chr(65 + i), name=name)
|
||||
for i, name in enumerate(model_filenames)]
|
||||
|
||||
model_elems = "\n\t\t\t".join(model_elems)
|
||||
eval_string = """
|
||||
<EVAL ID="{task_id}">
|
||||
<MODEL-ROOT>{model_root}</MODEL-ROOT>
|
||||
<PEER-ROOT>{peer_root}</PEER-ROOT>
|
||||
<INPUT-FORMAT TYPE="SEE">
|
||||
</INPUT-FORMAT>
|
||||
<PEERS>
|
||||
{peer_elems}
|
||||
</PEERS>
|
||||
<MODELS>
|
||||
{model_elems}
|
||||
</MODELS>
|
||||
</EVAL>
|
||||
""".format(
|
||||
task_id=task_id,
|
||||
model_root=model_dir, model_elems=model_elems,
|
||||
peer_root=system_dir, peer_elems=peer_elems)
|
||||
return eval_string
|
||||
|
||||
def __process_summaries(self, process_func):
|
||||
"""
|
||||
Helper method that applies process_func to the files in the
|
||||
system and model folders and saves the resulting files to new
|
||||
system and model folders.
|
||||
|
||||
"""
|
||||
temp_dir = mkdtemp(dir=self.temp_dir)
|
||||
new_system_dir = os.path.join(temp_dir, "system")
|
||||
os.mkdir(new_system_dir)
|
||||
new_model_dir = os.path.join(temp_dir, "model")
|
||||
os.mkdir(new_model_dir)
|
||||
self.log.info(
|
||||
"Processing summaries. Saving system files to {} and "
|
||||
"model files to {}.".format(new_system_dir, new_model_dir))
|
||||
process_func(self._system_dir, new_system_dir)
|
||||
process_func(self._model_dir, new_model_dir)
|
||||
self._system_dir = new_system_dir
|
||||
self._model_dir = new_model_dir
|
||||
|
||||
def __write_summaries(self):
|
||||
self.log.info("Writing summaries.")
|
||||
self.__process_summaries(self.convert_summaries_to_rouge_format)
|
||||
|
||||
@staticmethod
|
||||
def __get_model_filenames_for_id(id, model_dir, model_filenames_pattern):
|
||||
pattern = re.compile(model_filenames_pattern.replace('#ID#', id))
|
||||
model_filenames = [
|
||||
f for f in os.listdir(model_dir) if pattern.match(f)]
|
||||
if not model_filenames:
|
||||
raise Exception(
|
||||
"Could not find any model summaries for the system"
|
||||
" summary with ID {}. Specified model filename pattern was: "
|
||||
"{}".format(id, model_filenames_pattern))
|
||||
return model_filenames
|
||||
|
||||
def __get_options(self, rouge_args=None):
|
||||
"""
|
||||
Get supplied command line arguments for ROUGE or use default
|
||||
ones.
|
||||
|
||||
"""
|
||||
if self.args:
|
||||
options = self.args.split()
|
||||
elif rouge_args:
|
||||
options = rouge_args.split()
|
||||
else:
|
||||
options = [
|
||||
'-e', self._data_dir,
|
||||
'-c', 95,
|
||||
# '-2',
|
||||
# '-1',
|
||||
# '-U',
|
||||
'-m',
|
||||
# '-v',
|
||||
'-r', 1000,
|
||||
'-n', 2,
|
||||
# '-w', 1.2,
|
||||
'-a',
|
||||
]
|
||||
options = list(map(str, options))
|
||||
|
||||
options = self.__add_config_option(options)
|
||||
return options
|
||||
|
||||
def __create_dir_property(self, dir_name, docstring):
|
||||
"""
|
||||
Generate getter and setter for a directory property.
|
||||
|
||||
"""
|
||||
property_name = "{}_dir".format(dir_name)
|
||||
private_name = "_" + property_name
|
||||
setattr(self, private_name, None)
|
||||
|
||||
def fget(self):
|
||||
return getattr(self, private_name)
|
||||
|
||||
def fset(self, path):
|
||||
verify_dir(path, dir_name)
|
||||
setattr(self, private_name, path)
|
||||
|
||||
p = property(fget=fget, fset=fset, doc=docstring)
|
||||
setattr(self.__class__, property_name, p)
|
||||
|
||||
def __set_dir_properties(self):
|
||||
"""
|
||||
Automatically generate the properties for directories.
|
||||
|
||||
"""
|
||||
directories = [
|
||||
("home", "The ROUGE home directory."),
|
||||
("data", "The path of the ROUGE 'data' directory."),
|
||||
("system", "Path of the directory containing system summaries."),
|
||||
("model", "Path of the directory containing model summaries."),
|
||||
]
|
||||
for (dirname, docstring) in directories:
|
||||
self.__create_dir_property(dirname, docstring)
|
||||
|
||||
def __clean_rouge_args(self, rouge_args):
|
||||
"""
|
||||
Remove enclosing quotation marks, if any.
|
||||
|
||||
"""
|
||||
if not rouge_args:
|
||||
return
|
||||
quot_mark_pattern = re.compile('"(.+)"')
|
||||
match = quot_mark_pattern.match(rouge_args)
|
||||
if match:
|
||||
cleaned_args = match.group(1)
|
||||
return cleaned_args
|
||||
else:
|
||||
return rouge_args
|
||||
|
||||
def __add_config_option(self, options):
|
||||
return options + [self._config_file]
|
||||
|
||||
def __get_config_path(self):
|
||||
if platform.system() == "Windows":
|
||||
parent_dir = os.getenv("APPDATA")
|
||||
config_dir_name = "pyrouge"
|
||||
elif os.name == "posix":
|
||||
parent_dir = os.path.expanduser("~")
|
||||
config_dir_name = ".pyrouge"
|
||||
else:
|
||||
parent_dir = os.path.dirname(__file__)
|
||||
config_dir_name = ""
|
||||
config_dir = os.path.join(parent_dir, config_dir_name)
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir)
|
||||
return os.path.join(config_dir, 'settings.ini')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
from utils.argparsers import rouge_path_parser
|
||||
|
||||
parser = argparse.ArgumentParser(parents=[rouge_path_parser])
|
||||
args = parser.parse_args()
|
||||
|
||||
rouge = Rouge155(args.rouge_home)
|
||||
rouge.save_home_dir()
|
||||
@@ -0,0 +1,341 @@
|
||||
"""BERT finetuning runner."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import logging
|
||||
import glob
|
||||
import json
|
||||
import argparse
|
||||
import math
|
||||
import string
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from tqdm import tqdm, trange
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
# pip install py-rouge
|
||||
import rouge
|
||||
import time
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from pytorch_pretrained_bert.tokenization import BertTokenizer
|
||||
# pip install pyrouge
|
||||
from gigaword.bs_pyrouge import Rouge155
|
||||
|
||||
|
||||
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
|
||||
datefmt='%m/%d/%Y %H:%M:%S',
|
||||
level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# Required parameters
|
||||
parser.add_argument("--gold", type=str, help="Gold output file.")
|
||||
parser.add_argument("--pred", type=str, help="Input prediction file.")
|
||||
parser.add_argument("--split", type=str, default="",
|
||||
help="Data split (train/dev/test).")
|
||||
parser.add_argument("--save_best", action='store_true',
|
||||
help="Save best epoch.")
|
||||
parser.add_argument("--only_eval_best", action='store_true',
|
||||
help="Only evaluate best epoch.")
|
||||
parser.add_argument("--trunc_len", type=int, default=0,
|
||||
help="Truncate line by the maximum length.")
|
||||
default_process_count = max(1, cpu_count() - 1)
|
||||
parser.add_argument("--processes", type=int, default=default_process_count,
|
||||
help="Number of processes to use (default %(default)s)")
|
||||
parser.add_argument("--perl", action='store_true',
|
||||
help="Using the perl script.")
|
||||
parser.add_argument('--lazy_eval', action='store_true',
|
||||
help="Skip evaluation if the .rouge file exists.")
|
||||
args = parser.parse_args()
|
||||
|
||||
SPECIAL_TOKEN = ["[UNK]", "[PAD]", "[CLS]", "[MASK]"]
|
||||
evaluator = rouge.Rouge(metrics=['rouge-n', 'rouge-l'], max_n=2,
|
||||
limit_length=False, apply_avg=True, weight_factor=1.2)
|
||||
|
||||
|
||||
def test_rouge(cand, ref):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
candidates = cand
|
||||
references = ref
|
||||
assert len(candidates) == len(references)
|
||||
|
||||
cnt = len(candidates)
|
||||
current_time = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime())
|
||||
tmp_dir = os.path.join(temp_dir, "rouge-tmp-{}".format(current_time))
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
os.mkdir(tmp_dir + "/candidate")
|
||||
os.mkdir(tmp_dir + "/reference")
|
||||
try:
|
||||
for i in range(cnt):
|
||||
if len(references[i]) < 1:
|
||||
continue
|
||||
with open(tmp_dir + "/candidate/cand.{}.txt".format(i), "w",
|
||||
encoding="utf-8") as f:
|
||||
f.write(candidates[i])
|
||||
with open(tmp_dir + "/reference/ref.{}.txt".format(i), "w",
|
||||
encoding="utf-8") as f:
|
||||
f.write(references[i])
|
||||
r = Rouge155(temp_dir=temp_dir)
|
||||
r.model_dir = tmp_dir + "/reference/"
|
||||
r.system_dir = tmp_dir + "/candidate/"
|
||||
r.model_filename_pattern = 'ref.#ID#.txt'
|
||||
r.system_filename_pattern = r'cand.(\d+).txt'
|
||||
rouge_results = r.convert_and_evaluate()
|
||||
print(rouge_results)
|
||||
results_dict = r.output_to_dict(rouge_results)
|
||||
finally:
|
||||
if os.path.isdir(tmp_dir):
|
||||
shutil.rmtree(tmp_dir)
|
||||
return results_dict
|
||||
|
||||
|
||||
def rouge_results_to_str(results_dict):
|
||||
return ">> ROUGE-F(1/2/l): {:.2f}/{:.2f}/{:.2f}\nROUGE-R(1/2/3/l): {:.2f}/{:.2f}/{:.2f}\n".format(
|
||||
results_dict["rouge_1_f_score"] * 100,
|
||||
results_dict["rouge_2_f_score"] * 100,
|
||||
results_dict["rouge_l_f_score"] * 100,
|
||||
results_dict["rouge_1_recall"] * 100,
|
||||
results_dict["rouge_2_recall"] * 100,
|
||||
results_dict["rouge_l_recall"] * 100
|
||||
)
|
||||
|
||||
|
||||
def count_tokens(tokens):
|
||||
counter = {}
|
||||
for t in tokens:
|
||||
if t in counter.keys():
|
||||
counter[t] += 1
|
||||
else:
|
||||
counter[t] = 1
|
||||
return counter
|
||||
|
||||
|
||||
def get_f1(text_a, text_b):
|
||||
tokens_a = text_a.lower().split()
|
||||
tokens_b = text_b.lower().split()
|
||||
if len(tokens_a) == 0 or len(tokens_b) == 0:
|
||||
return 1 if len(tokens_a) == len(tokens_b) else 0
|
||||
set_a = count_tokens(tokens_a)
|
||||
set_b = count_tokens(tokens_b)
|
||||
match = 0
|
||||
for token in set_a.keys():
|
||||
if token in set_b.keys():
|
||||
match += min(set_a[token], set_b[token])
|
||||
p = match / len(tokens_a)
|
||||
r = match / len(tokens_b)
|
||||
return 2.0 * p * r / (p + r + 1e-5)
|
||||
|
||||
|
||||
_tok_dict = {"(": "-lrb-", ")": "-rrb-",
|
||||
"[": "-lsb-", "]": "-rsb-",
|
||||
"{": "-lcb-", "}": "-rcb-",
|
||||
"[UNK]": "UNK", '&': '&', '<': '<', '>': '>'}
|
||||
|
||||
|
||||
def _is_digit(w):
|
||||
for ch in w:
|
||||
if not(ch.isdigit() or ch == ','):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def fix_tokenization(text):
|
||||
input_tokens = text.split()
|
||||
output_tokens = []
|
||||
has_left_quote = False
|
||||
has_left_single_quote = False
|
||||
|
||||
i = 0
|
||||
prev_dash = False
|
||||
while i < len(input_tokens):
|
||||
tok = input_tokens[i]
|
||||
flag_prev_dash = False
|
||||
if tok in _tok_dict.keys():
|
||||
output_tokens.append(_tok_dict[tok])
|
||||
i += 1
|
||||
elif tok == "\"":
|
||||
if has_left_quote:
|
||||
output_tokens.append("''")
|
||||
else:
|
||||
output_tokens.append("``")
|
||||
has_left_quote = not has_left_quote
|
||||
i += 1
|
||||
elif tok == "'" and len(output_tokens) > 0 and output_tokens[-1].endswith("n") and i < len(input_tokens) - 1 and input_tokens[i + 1] == "t":
|
||||
output_tokens[-1] = output_tokens[-1][:-1]
|
||||
output_tokens.append("n't")
|
||||
i += 2
|
||||
elif tok == "'" and i < len(input_tokens) - 1 and input_tokens[i + 1] in ("s", "d", "ll"):
|
||||
output_tokens.append("'"+input_tokens[i + 1])
|
||||
i += 2
|
||||
elif tok == "'":
|
||||
if has_left_single_quote:
|
||||
output_tokens.append("'")
|
||||
else:
|
||||
output_tokens.append("`")
|
||||
has_left_single_quote = not has_left_single_quote
|
||||
i += 1
|
||||
elif tok == "." and i < len(input_tokens) - 2 and input_tokens[i + 1] == "." and input_tokens[i + 2] == ".":
|
||||
output_tokens.append("...")
|
||||
i += 3
|
||||
elif tok == "," and len(output_tokens) > 0 and _is_digit(output_tokens[-1]) and i < len(input_tokens) - 1 and _is_digit(input_tokens[i + 1]):
|
||||
# $ 3 , 000 -> $ 3,000
|
||||
output_tokens[-1] += ','+input_tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "." and len(output_tokens) > 0 and output_tokens[-1].isdigit() and i < len(input_tokens) - 1 and input_tokens[i + 1].isdigit():
|
||||
# 3 . 03 -> $ 3.03
|
||||
output_tokens[-1] += '.'+input_tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "." and len(output_tokens) > 0 and len(output_tokens[-1]) == 1 and output_tokens[-1].isupper() and i < len(input_tokens) - 2 and len(input_tokens[i + 1]) == 1 and input_tokens[i + 1].isupper() and input_tokens[i + 2] == '.':
|
||||
# U . N . -> U.N.
|
||||
k = i+3
|
||||
while k+2 < len(input_tokens):
|
||||
if len(input_tokens[k + 1]) == 1 and input_tokens[k + 1].isupper() and input_tokens[k + 2] == '.':
|
||||
k += 2
|
||||
else:
|
||||
break
|
||||
output_tokens[-1] += ''.join(input_tokens[i:k])
|
||||
i += 2
|
||||
elif tok == "-":
|
||||
if i < len(input_tokens) - 1 and input_tokens[i + 1] == "-":
|
||||
output_tokens.append("--")
|
||||
i += 2
|
||||
elif i == len(input_tokens) - 1 or i == 0:
|
||||
output_tokens.append("-")
|
||||
i += 1
|
||||
elif output_tokens[-1] not in string.punctuation and input_tokens[i + 1][0] not in string.punctuation:
|
||||
output_tokens[-1] += "-"
|
||||
i += 1
|
||||
flag_prev_dash = True
|
||||
else:
|
||||
output_tokens.append("-")
|
||||
i += 1
|
||||
elif prev_dash and len(output_tokens) > 0 and tok[0] not in string.punctuation:
|
||||
output_tokens[-1] += tok
|
||||
i += 1
|
||||
else:
|
||||
output_tokens.append(tok)
|
||||
i += 1
|
||||
prev_dash = flag_prev_dash
|
||||
return " ".join(output_tokens)
|
||||
|
||||
|
||||
def process_eval(eval_fn):
|
||||
gold_list = []
|
||||
with open(args.gold, "r", encoding="utf-8") as f_in:
|
||||
for l in f_in:
|
||||
line = l.strip()
|
||||
gold_list.append(line)
|
||||
|
||||
pred_list = []
|
||||
with open(eval_fn, "r", encoding="utf-8") as f_in:
|
||||
for l in f_in:
|
||||
buf = []
|
||||
sentence = fix_tokenization(l.strip()).replace('1', '#')
|
||||
buf.append(sentence)
|
||||
if args.trunc_len:
|
||||
num_left = args.trunc_len
|
||||
trunc_list = []
|
||||
for bit in buf:
|
||||
tk_list = bit.split()
|
||||
n = min(len(tk_list), num_left)
|
||||
trunc_list.append(' '.join(tk_list[:n]))
|
||||
num_left -= n
|
||||
if num_left <= 0:
|
||||
break
|
||||
else:
|
||||
trunc_list = buf
|
||||
line = "\n".join(trunc_list)
|
||||
pred_list.append(line)
|
||||
with open(eval_fn+'.post', 'w', encoding='utf-8') as f_out:
|
||||
for l in pred_list:
|
||||
f_out.write(l.strip())
|
||||
f_out.write('\n')
|
||||
# rouge scores
|
||||
if len(pred_list) < len(gold_list):
|
||||
# evaluate subset
|
||||
gold_list = gold_list[:len(pred_list)]
|
||||
assert len(pred_list) == len(gold_list)
|
||||
if args.perl:
|
||||
scores = test_rouge(pred_list, gold_list)
|
||||
else:
|
||||
scores = evaluator.get_scores(pred_list, [[it] for it in gold_list])
|
||||
return eval_fn, scores
|
||||
|
||||
|
||||
def main():
|
||||
if args.perl:
|
||||
eval_fn_list = list(glob.glob(args.pred))
|
||||
else:
|
||||
eval_fn_list = [eval_fn for eval_fn in glob.glob(args.pred) if not(
|
||||
args.lazy_eval and Path(eval_fn+".rouge").exists())]
|
||||
eval_fn_list = list(filter(lambda fn: not(fn.endswith(
|
||||
'.post') or fn.endswith('.rouge')), eval_fn_list))
|
||||
|
||||
if args.only_eval_best:
|
||||
best_epoch_dict = {}
|
||||
for dir_path in set(Path(fn).parent for fn in eval_fn_list):
|
||||
fn_save = os.path.join(dir_path, 'save_best.dev')
|
||||
if Path(fn_save).exists():
|
||||
with open(fn_save, 'r') as f_in:
|
||||
__, o_name, __ = f_in.read().strip().split('\n')
|
||||
epoch = o_name.split('.')[1]
|
||||
best_epoch_dict[dir_path] = epoch
|
||||
new_eval_fn_list = []
|
||||
for fn in eval_fn_list:
|
||||
dir_path = Path(fn).parent
|
||||
if dir_path in best_epoch_dict:
|
||||
if Path(fn).name.split('.')[1] == best_epoch_dict[dir_path]:
|
||||
new_eval_fn_list.append(fn)
|
||||
eval_fn_list = new_eval_fn_list
|
||||
|
||||
logger.info("***** Evaluation: %s *****", ','.join(eval_fn_list))
|
||||
num_pool = min(args.processes, len(eval_fn_list))
|
||||
p = Pool(num_pool)
|
||||
r_list = p.imap_unordered(process_eval, eval_fn_list)
|
||||
r_list = sorted([(fn, scores)
|
||||
for fn, scores in r_list], key=lambda x: x[0])
|
||||
rg2_dict = {}
|
||||
for fn, scores in r_list:
|
||||
print(fn)
|
||||
if args.perl:
|
||||
print(rouge_results_to_str(scores))
|
||||
else:
|
||||
rg2_dict[fn] = scores['rouge-2']['f']
|
||||
print(
|
||||
"ROUGE-1: {}\tROUGE-2: {}\n".format(scores['rouge-1']['f'], scores['rouge-2']['f']))
|
||||
with open(fn+".rouge", 'w') as f_out:
|
||||
f_out.write(json.dumps(
|
||||
{'rg1': scores['rouge-1']['f'], 'rg2': scores['rouge-2']['f']}))
|
||||
p.close()
|
||||
p.join()
|
||||
|
||||
if args.save_best:
|
||||
# find best results
|
||||
group_dict = {}
|
||||
for k, v in rg2_dict.items():
|
||||
d_name, o_name = Path(k).parent, Path(k).name
|
||||
if (d_name not in group_dict) or (v > group_dict[d_name][1]):
|
||||
group_dict[d_name] = (o_name, v)
|
||||
# compare and save the best result
|
||||
for k, v in group_dict.items():
|
||||
fn = os.path.join(k, 'save_best.'+args.split)
|
||||
o_name_s, rst_s = v
|
||||
should_save = True
|
||||
if Path(fn).exists():
|
||||
with open(fn, 'r') as f_in:
|
||||
rst_f = float(f_in.read().strip().split('\n')[-1])
|
||||
if rst_s <= rst_f:
|
||||
should_save = False
|
||||
if should_save:
|
||||
with open(fn, 'w') as f_out:
|
||||
f_out.write('{0}\n{1}\n{2}\n'.format(k, o_name_s, rst_s))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,114 @@
|
||||
import torch
|
||||
from torch.nn import DataParallel
|
||||
from torch.cuda._utils import _get_device_index
|
||||
from torch.nn.parallel._functions import Scatter
|
||||
from itertools import chain
|
||||
|
||||
|
||||
def scatter_imbalance(inputs, target_gpus, dim=0):
|
||||
r"""
|
||||
Slices tensors into approximately equal chunks and
|
||||
distributes them across given GPUs. Duplicates
|
||||
references to objects that are not tensors.
|
||||
"""
|
||||
def scatter_map(obj):
|
||||
if isinstance(obj, torch.Tensor):
|
||||
if (len(target_gpus) == 4) and (obj.size(dim) == 22):
|
||||
return Scatter.apply(target_gpus, (4, 6, 6, 6), dim, obj)
|
||||
if (len(target_gpus) == 4) and (obj.size(dim) == 60):
|
||||
return Scatter.apply(target_gpus, (12, 16, 16, 16), dim, obj)
|
||||
elif (len(target_gpus) == 4) and (obj.size(dim) == 144):
|
||||
return Scatter.apply(target_gpus, (24, 40, 40, 40), dim, obj)
|
||||
elif (len(target_gpus) == 8) and (obj.size(dim) == 46):
|
||||
return Scatter.apply(target_gpus, (4, 6, 6, 6, 6, 6, 6, 6), dim, obj)
|
||||
elif (len(target_gpus) == 8) and (obj.size(dim) == 62):
|
||||
return Scatter.apply(target_gpus, (6, 8, 8, 8, 8, 8, 8, 8), dim, obj)
|
||||
elif (len(target_gpus) == 8) and (obj.size(dim) == 94):
|
||||
return Scatter.apply(target_gpus, (10, 12, 12, 12, 12, 12, 12, 12), dim, obj)
|
||||
elif (len(target_gpus) == 8) and (obj.size(dim) == 110):
|
||||
return Scatter.apply(target_gpus, (12, 14, 14, 14, 14, 14, 14, 14), dim, obj)
|
||||
elif (len(target_gpus) == 8) and (obj.size(dim) == 118):
|
||||
return Scatter.apply(target_gpus, (13, 15, 15, 15, 15, 15, 15, 15), dim, obj)
|
||||
elif (len(target_gpus) == 8) and (obj.size(dim) == 126):
|
||||
return Scatter.apply(target_gpus, (14, 16, 16, 16, 16, 16, 16, 16), dim, obj)
|
||||
elif (len(target_gpus) == 8) and (obj.size(dim) == 134):
|
||||
return Scatter.apply(target_gpus, (15, 17, 17, 17, 17, 17, 17, 17), dim, obj)
|
||||
elif (len(target_gpus) == 8) and (obj.size(dim) == 142):
|
||||
return Scatter.apply(target_gpus, (16, 18, 18, 18, 18, 18, 18, 18), dim, obj)
|
||||
elif (len(target_gpus) == 16) and (obj.size(dim) == 222):
|
||||
return Scatter.apply(target_gpus, (12, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14), dim, obj)
|
||||
return Scatter.apply(target_gpus, None, dim, obj)
|
||||
if isinstance(obj, tuple) and len(obj) > 0:
|
||||
return list(zip(*map(scatter_map, obj)))
|
||||
if isinstance(obj, list) and len(obj) > 0:
|
||||
return list(map(list, zip(*map(scatter_map, obj))))
|
||||
if isinstance(obj, dict) and len(obj) > 0:
|
||||
return list(map(type(obj), zip(*map(scatter_map, obj.items()))))
|
||||
return [obj for targets in target_gpus]
|
||||
|
||||
# After scatter_map is called, a scatter_map cell will exist. This cell
|
||||
# has a reference to the actual function scatter_map, which has references
|
||||
# to a closure that has a reference to the scatter_map cell (because the
|
||||
# fn is recursive). To avoid this reference cycle, we set the function to
|
||||
# None, clearing the cell
|
||||
try:
|
||||
return scatter_map(inputs)
|
||||
finally:
|
||||
scatter_map = None
|
||||
|
||||
|
||||
def scatter_kwargs_imbalance(inputs, kwargs, target_gpus, dim=0):
|
||||
r"""Scatter with support for kwargs dictionary"""
|
||||
inputs = scatter_imbalance(inputs, target_gpus, dim) if inputs else []
|
||||
kwargs = scatter_imbalance(kwargs, target_gpus, dim) if kwargs else []
|
||||
if len(inputs) < len(kwargs):
|
||||
inputs.extend([() for _ in range(len(kwargs) - len(inputs))])
|
||||
elif len(kwargs) < len(inputs):
|
||||
kwargs.extend([{} for _ in range(len(inputs) - len(kwargs))])
|
||||
inputs = tuple(inputs)
|
||||
kwargs = tuple(kwargs)
|
||||
return inputs, kwargs
|
||||
|
||||
|
||||
class DataParallelImbalance(DataParallel):
|
||||
def __init__(self, module, device_ids=None, output_device=None, dim=0):
|
||||
super(DataParallelImbalance, self).__init__(
|
||||
module, device_ids, output_device, dim)
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
self.module = module
|
||||
self.device_ids = []
|
||||
return
|
||||
|
||||
if device_ids is None:
|
||||
device_ids = list(range(torch.cuda.device_count()))
|
||||
if output_device is None:
|
||||
output_device = device_ids[0]
|
||||
|
||||
if not all(t.is_cuda and t.device.index == device_ids[0]
|
||||
for t in chain(module.parameters(), module.buffers())):
|
||||
raise RuntimeError("module must have its parameters and buffers "
|
||||
"on device %d (device_ids[0])" % device_ids[0])
|
||||
|
||||
self.dim = dim
|
||||
self.module = module
|
||||
self.device_ids = list(
|
||||
map(lambda x: _get_device_index(x, True), device_ids))
|
||||
self.output_device = _get_device_index(output_device, True)
|
||||
|
||||
if len(self.device_ids) == 1:
|
||||
self.module.cuda(device_ids[0])
|
||||
|
||||
def forward(self, *inputs, **kwargs):
|
||||
if not self.device_ids:
|
||||
return self.module(*inputs, **kwargs)
|
||||
inputs, kwargs = self.scatter_imbalance(
|
||||
inputs, kwargs, self.device_ids)
|
||||
if len(self.device_ids) == 1:
|
||||
return self.module(*inputs[0], **kwargs[0])
|
||||
replicas = self.replicate(self.module, self.device_ids[:len(inputs)])
|
||||
outputs = self.parallel_apply(replicas, inputs, kwargs)
|
||||
return self.gather(outputs, self.output_device)
|
||||
|
||||
def scatter_imbalance(self, inputs, kwargs, device_ids):
|
||||
return scatter_kwargs_imbalance(inputs, kwargs, device_ids, dim=self.dim)
|
||||
@@ -0,0 +1,7 @@
|
||||
__version__ = "0.4.0"
|
||||
from .tokenization import BertTokenizer, BasicTokenizer, WordpieceTokenizer
|
||||
from .modeling import (BertConfig, BertModel, BertForPreTraining, BertForMaskedLM, BertForNextSentencePrediction, BertForSequenceClassification,
|
||||
BertForMultipleChoice, BertForTokenClassification, BertForQuestionAnswering, BertForPreTrainingLossMask, BertPreTrainingPairRel, BertPreTrainingPairTransform)
|
||||
from .optimization import BertAdam, BertAdamFineTune
|
||||
from .optimization_fp16 import FP16_Optimizer_State
|
||||
from .file_utils import PYTORCH_PRETRAINED_BERT_CACHE
|
||||
@@ -0,0 +1,22 @@
|
||||
# coding: utf8
|
||||
def main():
|
||||
import sys
|
||||
try:
|
||||
from .convert_tf_checkpoint_to_pytorch import convert_tf_checkpoint_to_pytorch
|
||||
except ModuleNotFoundError:
|
||||
print("pytorch_pretrained_bert can only be used from the commandline to convert TensorFlow models in PyTorch, "
|
||||
"In that case, it requires TensorFlow to be installed. Please see "
|
||||
"https://www.tensorflow.org/install/ for installation instructions.")
|
||||
raise
|
||||
|
||||
if len(sys.argv) != 5:
|
||||
# pylint: disable=line-too-long
|
||||
print("Should be used as `pytorch_pretrained_bert convert_tf_checkpoint_to_pytorch TF_CHECKPOINT TF_CONFIG PYTORCH_DUMP_OUTPUT`")
|
||||
else:
|
||||
PYTORCH_DUMP_OUTPUT = sys.argv.pop()
|
||||
TF_CONFIG = sys.argv.pop()
|
||||
TF_CHECKPOINT = sys.argv.pop()
|
||||
convert_tf_checkpoint_to_pytorch(TF_CHECKPOINT, TF_CONFIG, PYTORCH_DUMP_OUTPUT)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Utilities for working with the local dataset cache.
|
||||
This file is adapted from the AllenNLP library at https://github.com/allenai/allennlp
|
||||
Copyright by the AllenNLP authors.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
import tempfile
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union, IO, Callable, Set
|
||||
from hashlib import sha256
|
||||
from functools import wraps
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
PYTORCH_PRETRAINED_BERT_CACHE = Path(os.getenv('PYTORCH_PRETRAINED_BERT_CACHE',
|
||||
Path.home() / '.pytorch_pretrained_bert'))
|
||||
|
||||
|
||||
def url_to_filename(url: str, etag: str = None) -> str:
|
||||
"""
|
||||
Convert `url` into a hashed filename in a repeatable way.
|
||||
If `etag` is specified, append its hash to the url's, delimited
|
||||
by a period.
|
||||
"""
|
||||
url_bytes = url.encode('utf-8')
|
||||
url_hash = sha256(url_bytes)
|
||||
filename = url_hash.hexdigest()
|
||||
|
||||
if etag:
|
||||
etag_bytes = etag.encode('utf-8')
|
||||
etag_hash = sha256(etag_bytes)
|
||||
filename += '.' + etag_hash.hexdigest()
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def filename_to_url(filename: str, cache_dir: Union[str, Path] = None) -> Tuple[str, str]:
|
||||
"""
|
||||
Return the url and etag (which may be ``None``) stored for `filename`.
|
||||
Raise ``FileNotFoundError`` if `filename` or its stored metadata do not exist.
|
||||
"""
|
||||
if cache_dir is None:
|
||||
cache_dir = PYTORCH_PRETRAINED_BERT_CACHE
|
||||
if isinstance(cache_dir, Path):
|
||||
cache_dir = str(cache_dir)
|
||||
|
||||
cache_path = os.path.join(cache_dir, filename)
|
||||
if not os.path.exists(cache_path):
|
||||
raise FileNotFoundError("file {} not found".format(cache_path))
|
||||
|
||||
meta_path = cache_path + '.json'
|
||||
if not os.path.exists(meta_path):
|
||||
raise FileNotFoundError("file {} not found".format(meta_path))
|
||||
|
||||
with open(meta_path) as meta_file:
|
||||
metadata = json.load(meta_file)
|
||||
url = metadata['url']
|
||||
etag = metadata['etag']
|
||||
|
||||
return url, etag
|
||||
|
||||
|
||||
def cached_path(url_or_filename: Union[str, Path], cache_dir: Union[str, Path] = None) -> str:
|
||||
"""
|
||||
Given something that might be a URL (or might be a local path),
|
||||
determine which. If it's a URL, download the file and cache it, and
|
||||
return the path to the cached file. If it's already a local path,
|
||||
make sure the file exists and then return the path.
|
||||
"""
|
||||
if cache_dir is None:
|
||||
cache_dir = PYTORCH_PRETRAINED_BERT_CACHE
|
||||
if isinstance(url_or_filename, Path):
|
||||
url_or_filename = str(url_or_filename)
|
||||
if isinstance(cache_dir, Path):
|
||||
cache_dir = str(cache_dir)
|
||||
|
||||
parsed = urlparse(url_or_filename)
|
||||
|
||||
if parsed.scheme in ('http', 'https', 's3'):
|
||||
# URL, so get it from the cache (downloading if necessary)
|
||||
return get_from_cache(url_or_filename, cache_dir)
|
||||
elif os.path.exists(url_or_filename):
|
||||
# File, and it exists.
|
||||
return url_or_filename
|
||||
elif parsed.scheme == '':
|
||||
# File, but it doesn't exist.
|
||||
raise FileNotFoundError("file {} not found".format(url_or_filename))
|
||||
else:
|
||||
# Something unknown
|
||||
raise ValueError("unable to parse {} as a URL or as a local path".format(url_or_filename))
|
||||
|
||||
|
||||
def split_s3_path(url: str) -> Tuple[str, str]:
|
||||
"""Split a full s3 path into the bucket name and path."""
|
||||
parsed = urlparse(url)
|
||||
if not parsed.netloc or not parsed.path:
|
||||
raise ValueError("bad s3 path {}".format(url))
|
||||
bucket_name = parsed.netloc
|
||||
s3_path = parsed.path
|
||||
# Remove '/' at beginning of path.
|
||||
if s3_path.startswith("/"):
|
||||
s3_path = s3_path[1:]
|
||||
return bucket_name, s3_path
|
||||
|
||||
|
||||
def s3_request(func: Callable):
|
||||
"""
|
||||
Wrapper function for s3 requests in order to create more helpful error
|
||||
messages.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(url: str, *args, **kwargs):
|
||||
try:
|
||||
return func(url, *args, **kwargs)
|
||||
except ClientError as exc:
|
||||
if int(exc.response["Error"]["Code"]) == 404:
|
||||
raise FileNotFoundError("file {} not found".format(url))
|
||||
else:
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@s3_request
|
||||
def s3_etag(url: str) -> Optional[str]:
|
||||
"""Check ETag on S3 object."""
|
||||
s3_resource = boto3.resource("s3")
|
||||
bucket_name, s3_path = split_s3_path(url)
|
||||
s3_object = s3_resource.Object(bucket_name, s3_path)
|
||||
return s3_object.e_tag
|
||||
|
||||
|
||||
@s3_request
|
||||
def s3_get(url: str, temp_file: IO) -> None:
|
||||
"""Pull a file directly from S3."""
|
||||
s3_resource = boto3.resource("s3")
|
||||
bucket_name, s3_path = split_s3_path(url)
|
||||
s3_resource.Bucket(bucket_name).download_fileobj(s3_path, temp_file)
|
||||
|
||||
|
||||
def http_get(url: str, temp_file: IO) -> None:
|
||||
req = requests.get(url, stream=True)
|
||||
content_length = req.headers.get('Content-Length')
|
||||
total = int(content_length) if content_length is not None else None
|
||||
progress = tqdm(unit="B", total=total)
|
||||
for chunk in req.iter_content(chunk_size=1024):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
progress.update(len(chunk))
|
||||
temp_file.write(chunk)
|
||||
progress.close()
|
||||
|
||||
|
||||
def get_from_cache(url: str, cache_dir: Union[str, Path] = None) -> str:
|
||||
"""
|
||||
Given a URL, look for the corresponding dataset in the local cache.
|
||||
If it's not there, download it. Then return the path to the cached file.
|
||||
"""
|
||||
if cache_dir is None:
|
||||
cache_dir = PYTORCH_PRETRAINED_BERT_CACHE
|
||||
if isinstance(cache_dir, Path):
|
||||
cache_dir = str(cache_dir)
|
||||
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
# Get eTag to add to filename, if it exists.
|
||||
if url.startswith("s3://"):
|
||||
etag = s3_etag(url)
|
||||
else:
|
||||
response = requests.head(url, allow_redirects=True)
|
||||
if response.status_code != 200:
|
||||
raise IOError("HEAD request failed for url {} with status code {}"
|
||||
.format(url, response.status_code))
|
||||
etag = response.headers.get("ETag")
|
||||
|
||||
filename = url_to_filename(url, etag)
|
||||
|
||||
# get cache path to put the file
|
||||
cache_path = os.path.join(cache_dir, filename)
|
||||
|
||||
if not os.path.exists(cache_path):
|
||||
# Download to temporary file, then copy to cache dir once finished.
|
||||
# Otherwise you get corrupt cache entries if the download gets interrupted.
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
logger.info("%s not found in cache, downloading to %s", url, temp_file.name)
|
||||
|
||||
# GET file object
|
||||
if url.startswith("s3://"):
|
||||
s3_get(url, temp_file)
|
||||
else:
|
||||
http_get(url, temp_file)
|
||||
|
||||
# we are copying the file before closing it, so flush to avoid truncation
|
||||
temp_file.flush()
|
||||
# shutil.copyfileobj() starts at the current position, so go to the start
|
||||
temp_file.seek(0)
|
||||
|
||||
logger.info("copying %s to cache at %s", temp_file.name, cache_path)
|
||||
with open(cache_path, 'wb') as cache_file:
|
||||
shutil.copyfileobj(temp_file, cache_file)
|
||||
|
||||
logger.info("creating metadata file for %s", cache_path)
|
||||
meta = {'url': url, 'etag': etag}
|
||||
meta_path = cache_path + '.json'
|
||||
with open(meta_path, 'w') as meta_file:
|
||||
json.dump(meta, meta_file)
|
||||
|
||||
logger.info("removing temp file %s", temp_file.name)
|
||||
|
||||
return cache_path
|
||||
|
||||
|
||||
def read_set_from_file(filename: str) -> Set[str]:
|
||||
'''
|
||||
Extract a de-duped collection (set) of text from a file.
|
||||
Expected file format is one item per line.
|
||||
'''
|
||||
collection = set()
|
||||
with open(filename, 'r', encoding='utf-8') as file_:
|
||||
for line in file_:
|
||||
collection.add(line.rstrip())
|
||||
return collection
|
||||
|
||||
|
||||
def get_file_extension(path: str, dot=True, lower: bool = True):
|
||||
ext = os.path.splitext(path)[1]
|
||||
ext = ext if dot else ext[1:]
|
||||
return ext.lower() if lower else ext
|
||||
@@ -0,0 +1,48 @@
|
||||
# coding=utf-8
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch.nn.modules.loss import _Loss
|
||||
|
||||
|
||||
class LabelSmoothingLoss(_Loss):
|
||||
"""
|
||||
With label smoothing,
|
||||
KL-divergence between q_{smoothed ground truth prob.}(w)
|
||||
and p_{prob. computed by model}(w) is minimized.
|
||||
"""
|
||||
|
||||
def __init__(self, label_smoothing=0, tgt_vocab_size=0, ignore_index=0, size_average=None, reduce=None, reduction='mean'):
|
||||
assert 0.0 < label_smoothing <= 1.0
|
||||
self.ignore_index = ignore_index
|
||||
super(LabelSmoothingLoss, self).__init__(
|
||||
size_average=size_average, reduce=reduce, reduction=reduction)
|
||||
|
||||
assert label_smoothing > 0
|
||||
assert tgt_vocab_size > 0
|
||||
|
||||
smoothing_value = label_smoothing / (tgt_vocab_size - 2)
|
||||
one_hot = torch.full((tgt_vocab_size,), smoothing_value)
|
||||
one_hot[self.ignore_index] = 0
|
||||
self.register_buffer('one_hot', one_hot.unsqueeze(0))
|
||||
self.confidence = 1.0 - label_smoothing
|
||||
self.tgt_vocab_size = tgt_vocab_size
|
||||
|
||||
def forward(self, output, target):
|
||||
"""
|
||||
output (FloatTensor): batch_size * num_pos * n_classes
|
||||
target (LongTensor): batch_size * num_pos
|
||||
"""
|
||||
assert self.tgt_vocab_size == output.size(2)
|
||||
batch_size, num_pos = target.size(0), target.size(1)
|
||||
output = output.view(-1, self.tgt_vocab_size)
|
||||
target = target.view(-1)
|
||||
model_prob = self.one_hot.repeat(target.size(0), 1)
|
||||
model_prob.scatter_(1, target.unsqueeze(1), self.confidence)
|
||||
model_prob.masked_fill_((target == self.ignore_index).unsqueeze(1), 0)
|
||||
|
||||
return F.kl_div(output, model_prob, reduction='none').view(batch_size, num_pos, -1).sum(2)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,401 @@
|
||||
# coding=utf-8
|
||||
# Copyright 2018 The Google AI Language Team Authors and The HugginFace Inc. team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""PyTorch optimization for BERT model."""
|
||||
|
||||
import math
|
||||
import torch
|
||||
from torch.optim import Optimizer
|
||||
from torch.optim.optimizer import required
|
||||
from torch.nn.utils import clip_grad_norm_
|
||||
|
||||
from collections import defaultdict
|
||||
from torch._six import container_abcs
|
||||
from copy import deepcopy
|
||||
from itertools import chain
|
||||
|
||||
|
||||
def warmup_cosine(x, warmup=0.002):
|
||||
if x < warmup:
|
||||
return x/warmup
|
||||
return 0.5 * (1.0 + torch.cos(math.pi * x))
|
||||
|
||||
|
||||
def warmup_constant(x, warmup=0.002):
|
||||
if x < warmup:
|
||||
return x/warmup
|
||||
return 1.0
|
||||
|
||||
|
||||
def warmup_linear(x, warmup=0.002):
|
||||
if x < warmup:
|
||||
return x/warmup
|
||||
return max((x-1.)/(warmup-1.), 0)
|
||||
|
||||
|
||||
SCHEDULES = {
|
||||
'warmup_cosine': warmup_cosine,
|
||||
'warmup_constant': warmup_constant,
|
||||
'warmup_linear': warmup_linear,
|
||||
}
|
||||
|
||||
|
||||
class BertAdam(Optimizer):
|
||||
"""Implements BERT version of Adam algorithm with weight decay fix.
|
||||
Params:
|
||||
lr: learning rate
|
||||
warmup: portion of t_total for the warmup, -1 means no warmup. Default: -1
|
||||
t_total: total number of training steps for the learning
|
||||
rate schedule, -1 means constant learning rate. Default: -1
|
||||
schedule: schedule to use for the warmup (see above). Default: 'warmup_linear'
|
||||
b1: Adams b1. Default: 0.9
|
||||
b2: Adams b2. Default: 0.999
|
||||
e: Adams epsilon. Default: 1e-6
|
||||
weight_decay: Weight decay. Default: 0.01
|
||||
max_grad_norm: Maximum norm for the gradients (-1 means no clipping). Default: 1.0
|
||||
"""
|
||||
|
||||
def __init__(self, params, lr=required, warmup=-1, t_total=-1, schedule='warmup_linear', b1=0.9, b2=0.999, e=1e-6, weight_decay=0.01, max_grad_norm=1.0):
|
||||
if lr is not required and lr < 0.0:
|
||||
raise ValueError(
|
||||
"Invalid learning rate: {} - should be >= 0.0".format(lr))
|
||||
if schedule not in SCHEDULES:
|
||||
raise ValueError("Invalid schedule parameter: {}".format(schedule))
|
||||
if not 0.0 <= warmup < 1.0 and not warmup == -1:
|
||||
raise ValueError(
|
||||
"Invalid warmup: {} - should be in [0.0, 1.0[ or -1".format(warmup))
|
||||
if not 0.0 <= b1 < 1.0:
|
||||
raise ValueError(
|
||||
"Invalid b1 parameter: {} - should be in [0.0, 1.0[".format(b1))
|
||||
if not 0.0 <= b2 < 1.0:
|
||||
raise ValueError(
|
||||
"Invalid b2 parameter: {} - should be in [0.0, 1.0[".format(b2))
|
||||
if not e >= 0.0:
|
||||
raise ValueError(
|
||||
"Invalid epsilon value: {} - should be >= 0.0".format(e))
|
||||
defaults = dict(lr=lr, schedule=schedule, warmup=warmup, t_total=t_total,
|
||||
b1=b1, b2=b2, e=e, weight_decay=weight_decay,
|
||||
max_grad_norm=max_grad_norm)
|
||||
super(BertAdam, self).__init__(params, defaults)
|
||||
|
||||
def get_lr(self):
|
||||
lr = []
|
||||
for group in self.param_groups:
|
||||
for p in group['params']:
|
||||
state = self.state[p]
|
||||
if len(state) == 0:
|
||||
return [0]
|
||||
if group['t_total'] != -1:
|
||||
schedule_fct = SCHEDULES[group['schedule']]
|
||||
lr_scheduled = group['lr'] * schedule_fct(
|
||||
state['step']/group['t_total'], group['warmup'])
|
||||
else:
|
||||
lr_scheduled = group['lr']
|
||||
lr.append(lr_scheduled)
|
||||
return lr
|
||||
|
||||
def step(self, closure=None):
|
||||
"""Performs a single optimization step.
|
||||
|
||||
Arguments:
|
||||
closure (callable, optional): A closure that reevaluates the model
|
||||
and returns the loss.
|
||||
"""
|
||||
loss = None
|
||||
if closure is not None:
|
||||
loss = closure()
|
||||
|
||||
for group in self.param_groups:
|
||||
for p in group['params']:
|
||||
if p.grad is None:
|
||||
continue
|
||||
grad = p.grad.data
|
||||
if grad.is_sparse:
|
||||
raise RuntimeError(
|
||||
'Adam does not support sparse gradients, please consider SparseAdam instead')
|
||||
|
||||
state = self.state[p]
|
||||
|
||||
# State initialization
|
||||
if len(state) == 0:
|
||||
state['step'] = 0
|
||||
# Exponential moving average of gradient values
|
||||
state['next_m'] = torch.zeros_like(p.data)
|
||||
# Exponential moving average of squared gradient values
|
||||
state['next_v'] = torch.zeros_like(p.data)
|
||||
|
||||
next_m, next_v = state['next_m'], state['next_v']
|
||||
beta1, beta2 = group['b1'], group['b2']
|
||||
|
||||
# Add grad clipping
|
||||
if group['max_grad_norm'] > 0:
|
||||
clip_grad_norm_(p, group['max_grad_norm'])
|
||||
|
||||
# Decay the first and second moment running average coefficient
|
||||
# In-place operations to update the averages at the same time
|
||||
next_m.mul_(beta1).add_(1 - beta1, grad)
|
||||
next_v.mul_(beta2).addcmul_(1 - beta2, grad, grad)
|
||||
update = next_m / (next_v.sqrt() + group['e'])
|
||||
|
||||
# Just adding the square of the weights to the loss function is *not*
|
||||
# the correct way of using L2 regularization/weight decay with Adam,
|
||||
# since that will interact with the m and v parameters in strange ways.
|
||||
#
|
||||
# Instead we want to decay the weights in a manner that doesn't interact
|
||||
# with the m/v parameters. This is equivalent to adding the square
|
||||
# of the weights to the loss with plain (non-momentum) SGD.
|
||||
if group['weight_decay'] > 0.0:
|
||||
update += group['weight_decay'] * p.data
|
||||
|
||||
if group['t_total'] != -1:
|
||||
schedule_fct = SCHEDULES[group['schedule']]
|
||||
lr_scheduled = group['lr'] * schedule_fct(
|
||||
state['step']/group['t_total'], group['warmup'])
|
||||
else:
|
||||
lr_scheduled = group['lr']
|
||||
|
||||
update_with_lr = lr_scheduled * update
|
||||
p.data.add_(-update_with_lr)
|
||||
|
||||
state['step'] += 1
|
||||
|
||||
# step_size = lr_scheduled * math.sqrt(bias_correction2) / bias_correction1
|
||||
# No bias correction
|
||||
# bias_correction1 = 1 - beta1 ** state['step']
|
||||
# bias_correction2 = 1 - beta2 ** state['step']
|
||||
|
||||
return loss
|
||||
|
||||
|
||||
class BertAdamFineTune(BertAdam):
|
||||
def __init__(self, params, lr=required, warmup=-1, t_total=-1, schedule='warmup_linear', b1=0.9, b2=0.999, e=1e-6, weight_decay=0.01, max_grad_norm=1.0):
|
||||
self.init_param_group = []
|
||||
super(BertAdamFineTune, self).__init__(params, lr, warmup,
|
||||
t_total, schedule, b1, b2, e, weight_decay, max_grad_norm)
|
||||
|
||||
def save_init_param_group(self, param_groups, name_groups, missing_keys):
|
||||
self.init_param_group = []
|
||||
for group, name in zip(param_groups, name_groups):
|
||||
if group['weight_decay'] > 0.0:
|
||||
init_p_list = []
|
||||
for p, n in zip(group['params'], name):
|
||||
init_p = p.data.clone().detach()
|
||||
if any(mk in n for mk in missing_keys):
|
||||
print("[no finetuning weight decay]", n)
|
||||
# should use the original weight decay
|
||||
init_p.zero_()
|
||||
init_p_list.append(init_p)
|
||||
self.init_param_group.append(init_p_list)
|
||||
else:
|
||||
# placeholder
|
||||
self.init_param_group.append([])
|
||||
|
||||
def step(self, closure=None):
|
||||
"""Performs a single optimization step.
|
||||
|
||||
Arguments:
|
||||
closure (callable, optional): A closure that reevaluates the model
|
||||
and returns the loss.
|
||||
"""
|
||||
loss = None
|
||||
if closure is not None:
|
||||
loss = closure()
|
||||
|
||||
for i_group, group in enumerate(self.param_groups):
|
||||
for i_p, p in enumerate(group['params']):
|
||||
if p.grad is None:
|
||||
continue
|
||||
grad = p.grad.data
|
||||
if grad.is_sparse:
|
||||
raise RuntimeError(
|
||||
'Adam does not support sparse gradients, please consider SparseAdam instead')
|
||||
|
||||
state = self.state[p]
|
||||
|
||||
# State initialization
|
||||
if len(state) == 0:
|
||||
state['step'] = 0
|
||||
# Exponential moving average of gradient values
|
||||
state['next_m'] = torch.zeros_like(p.data)
|
||||
# Exponential moving average of squared gradient values
|
||||
state['next_v'] = torch.zeros_like(p.data)
|
||||
|
||||
next_m, next_v = state['next_m'], state['next_v']
|
||||
beta1, beta2 = group['b1'], group['b2']
|
||||
|
||||
# Add grad clipping
|
||||
if group['max_grad_norm'] > 0:
|
||||
clip_grad_norm_(p, group['max_grad_norm'])
|
||||
|
||||
# Decay the first and second moment running average coefficient
|
||||
# In-place operations to update the averages at the same time
|
||||
next_m.mul_(beta1).add_(1 - beta1, grad)
|
||||
next_v.mul_(beta2).addcmul_(1 - beta2, grad, grad)
|
||||
update = next_m / (next_v.sqrt() + group['e'])
|
||||
|
||||
# Just adding the square of the weights to the loss function is *not*
|
||||
# the correct way of using L2 regularization/weight decay with Adam,
|
||||
# since that will interact with the m and v parameters in strange ways.
|
||||
#
|
||||
# Instead we want to decay the weights in a manner that doesn't interact
|
||||
# with the m/v parameters. This is equivalent to adding the square
|
||||
# of the weights to the loss with plain (non-momentum) SGD.
|
||||
if group['weight_decay'] > 0.0:
|
||||
if self.init_param_group:
|
||||
update += group['weight_decay'] * \
|
||||
(2.0 * p.data -
|
||||
self.init_param_group[i_group][i_p])
|
||||
else:
|
||||
update += group['weight_decay'] * p.data
|
||||
|
||||
if group['t_total'] != -1:
|
||||
schedule_fct = SCHEDULES[group['schedule']]
|
||||
lr_scheduled = group['lr'] * schedule_fct(
|
||||
state['step']/group['t_total'], group['warmup'])
|
||||
else:
|
||||
lr_scheduled = group['lr']
|
||||
|
||||
update_with_lr = lr_scheduled * update
|
||||
p.data.add_(-update_with_lr)
|
||||
|
||||
state['step'] += 1
|
||||
|
||||
# step_size = lr_scheduled * math.sqrt(bias_correction2) / bias_correction1
|
||||
# No bias correction
|
||||
# bias_correction1 = 1 - beta1 ** state['step']
|
||||
# bias_correction2 = 1 - beta2 ** state['step']
|
||||
|
||||
return loss
|
||||
|
||||
def load_state_dict_subset_finetune(self, state_dict, num_load_group):
|
||||
r"""Loads the optimizer state.
|
||||
|
||||
Arguments:
|
||||
state_dict (dict): optimizer state. Should be an object returned
|
||||
from a call to :meth:`state_dict`.
|
||||
"""
|
||||
# deepcopy, to be consistent with module API
|
||||
state_dict = deepcopy(state_dict)
|
||||
# Validate the state_dict
|
||||
groups = self.param_groups
|
||||
saved_groups = state_dict['param_groups']
|
||||
|
||||
if len(groups) < num_load_group or len(saved_groups) < num_load_group:
|
||||
raise ValueError("loaded state dict has a different number of "
|
||||
"parameter groups")
|
||||
param_lens = (len(g['params']) for g in groups[:num_load_group])
|
||||
saved_lens = (len(g['params']) for g in saved_groups[:num_load_group])
|
||||
if any(p_len != s_len for p_len, s_len in zip(param_lens, saved_lens)):
|
||||
raise ValueError("loaded state dict contains a parameter group "
|
||||
"that doesn't match the size of optimizer's group")
|
||||
|
||||
# Update the state
|
||||
id_map = {old_id: p for old_id, p in
|
||||
zip(chain(*(g['params'] for g in saved_groups[:num_load_group])),
|
||||
chain(*(g['params'] for g in groups[:num_load_group])))}
|
||||
|
||||
def cast(param, value):
|
||||
r"""Make a deep copy of value, casting all tensors to device of param."""
|
||||
if isinstance(value, torch.Tensor):
|
||||
# Floating-point types are a bit special here. They are the only ones
|
||||
# that are assumed to always match the type of params.
|
||||
if param.is_floating_point():
|
||||
value = value.to(param.dtype)
|
||||
value = value.to(param.device)
|
||||
return value
|
||||
elif isinstance(value, dict):
|
||||
return {k: cast(param, v) for k, v in value.items()}
|
||||
elif isinstance(value, container_abcs.Iterable):
|
||||
return type(value)(cast(param, v) for v in value)
|
||||
else:
|
||||
return value
|
||||
|
||||
# Copy state assigned to params (and cast tensors to appropriate types).
|
||||
# State that is not assigned to params is copied as is (needed for
|
||||
# backward compatibility).
|
||||
state = defaultdict(dict)
|
||||
for k, v in state_dict['state'].items():
|
||||
if k in id_map:
|
||||
param = id_map[k]
|
||||
state[param] = cast(param, v)
|
||||
else:
|
||||
state[k] = v
|
||||
# handle additional params
|
||||
for k, v in self.state:
|
||||
if k not in state:
|
||||
state[k] = v
|
||||
|
||||
# do not change groups: {'weight_decay': 0.01, 'lr': 9.995e-06, 'schedule': 'warmup_linear', 'warmup': 0.1, 't_total': 400000, 'b1': 0.9, 'b2': 0.999, 'e': 1e-06, 'max_grad_norm': 1.0, 'params': [...]}
|
||||
# # Update parameter groups, setting their 'params' value
|
||||
# def update_group(group, new_group):
|
||||
# new_group['params'] = group['params']
|
||||
# return new_group
|
||||
# param_groups = [
|
||||
# update_group(g, ng) for g, ng in zip(groups[:num_load_group], saved_groups[:num_load_group])]
|
||||
# # handle additional params
|
||||
# param_groups.extend(groups[num_load_group:])
|
||||
|
||||
self.__setstate__({'state': state, 'param_groups': groups})
|
||||
|
||||
|
||||
def find_state_dict_subset_finetune(org_state_dict, org_name_list, no_decay, param_optimizer):
|
||||
# only use the bert encoder and embeddings
|
||||
want_name_set = set()
|
||||
for n in org_name_list:
|
||||
if ('bert.encoder' in n) or ('bert.embeddings' in n):
|
||||
want_name_set.add(n)
|
||||
# original: name to pid, pid to name
|
||||
org_grouped_names = [[n for n in org_name_list if not any(nd in n for nd in no_decay)],
|
||||
[n for n in org_name_list if any(nd in n for nd in no_decay)]]
|
||||
org_n2id, org_id2n = {}, {}
|
||||
for ng, pg in zip(org_grouped_names, org_state_dict['param_groups']):
|
||||
for n, pid in zip(ng, pg['params']):
|
||||
org_n2id[n] = pid
|
||||
org_id2n[pid] = n
|
||||
# group by: whether pretrained; whether weight decay
|
||||
g_np_list = [
|
||||
[(n, p) for n, p in param_optimizer if n in want_name_set and not any(
|
||||
nd in n for nd in no_decay)],
|
||||
[(n, p) for n, p in param_optimizer if n in want_name_set and any(
|
||||
nd in n for nd in no_decay)],
|
||||
[(n, p) for n, p in param_optimizer if n not in want_name_set and not any(
|
||||
nd in n for nd in no_decay)],
|
||||
[(n, p) for n, p in param_optimizer if n not in want_name_set and any(
|
||||
nd in n for nd in no_decay)],
|
||||
]
|
||||
optimizer_grouped_parameters = [
|
||||
{'params': [p for n, p in g_np_list[0]], 'weight_decay': 0.01},
|
||||
{'params': [p for n, p in g_np_list[1]], 'weight_decay': 0.0},
|
||||
{'params': [p for n, p in g_np_list[2]], 'weight_decay': 0.01},
|
||||
{'params': [p for n, p in g_np_list[3]], 'weight_decay': 0.0}
|
||||
]
|
||||
new_state_dict = {}
|
||||
# regroup the original state_dict
|
||||
new_state_dict['state'] = {pid: v for pid, v in org_state_dict['state'].items(
|
||||
) if pid not in org_id2n or org_id2n[pid] in want_name_set}
|
||||
# reset step count to 0
|
||||
for pid, st in new_state_dict['state'].items():
|
||||
st['step'] = 0
|
||||
|
||||
def _filter_group(group, g_np_list, i, org_n2id):
|
||||
packed = {k: v for k, v in group.items() if k != 'params'}
|
||||
packed['params'] = [pid for pid in group['params']
|
||||
if pid in org_id2n and org_id2n[pid] in want_name_set]
|
||||
assert len(g_np_list[i]) == len(packed['params'])
|
||||
# keep them the same order
|
||||
packed['params'] = [org_n2id[n] for n, p in g_np_list[i]]
|
||||
return packed
|
||||
new_state_dict['param_groups'] = [_filter_group(
|
||||
g, g_np_list, i, org_n2id) for i, g in enumerate(org_state_dict['param_groups'])]
|
||||
return new_state_dict, optimizer_grouped_parameters
|
||||
@@ -0,0 +1,80 @@
|
||||
# coding=utf-8
|
||||
"""PyTorch optimization for BERT model."""
|
||||
|
||||
from apex.optimizers import FP16_Optimizer
|
||||
|
||||
|
||||
class FP16_Optimizer_State(FP16_Optimizer):
|
||||
def __init__(self,
|
||||
init_optimizer,
|
||||
static_loss_scale=1.0,
|
||||
dynamic_loss_scale=False,
|
||||
dynamic_loss_args=None,
|
||||
verbose=True):
|
||||
super(FP16_Optimizer_State, self).__init__(init_optimizer,
|
||||
static_loss_scale, dynamic_loss_scale, dynamic_loss_args, verbose)
|
||||
|
||||
def state_dict(self):
|
||||
"""
|
||||
Returns a dict containing the current state of this :class:`FP16_Optimizer` instance.
|
||||
This dict contains attributes of :class:`FP16_Optimizer`, as well as the state_dict
|
||||
of the contained Pytorch optimizer.
|
||||
Example::
|
||||
checkpoint = {}
|
||||
checkpoint['model'] = model.state_dict()
|
||||
checkpoint['optimizer'] = optimizer.state_dict()
|
||||
torch.save(checkpoint, "saved.pth")
|
||||
"""
|
||||
state_dict = {}
|
||||
state_dict['dynamic_loss_scale'] = self.dynamic_loss_scale
|
||||
state_dict['cur_scale'] = self.cur_scale
|
||||
state_dict['cur_iter'] = self.cur_iter
|
||||
if state_dict['dynamic_loss_scale']:
|
||||
state_dict['last_overflow_iter'] = self.last_overflow_iter
|
||||
state_dict['scale_factor'] = self.scale_factor
|
||||
state_dict['scale_window'] = self.scale_window
|
||||
state_dict['optimizer_state_dict'] = self.optimizer.state_dict()
|
||||
state_dict['fp32_groups_flat'] = self.fp32_groups_flat
|
||||
return state_dict
|
||||
|
||||
def load_state_dict(self, state_dict):
|
||||
"""
|
||||
Loads a state_dict created by an earlier call to state_dict().
|
||||
If ``fp16_optimizer_instance`` was constructed from some ``init_optimizer``,
|
||||
whose parameters in turn came from ``model``, it is expected that the user
|
||||
will call ``model.load_state_dict()`` before
|
||||
``fp16_optimizer_instance.load_state_dict()`` is called.
|
||||
Example::
|
||||
model = torch.nn.Linear(D_in, D_out).cuda().half()
|
||||
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
|
||||
optimizer = FP16_Optimizer(optimizer, static_loss_scale = 128.0)
|
||||
...
|
||||
checkpoint = torch.load("saved.pth")
|
||||
model.load_state_dict(checkpoint['model'])
|
||||
optimizer.load_state_dict(checkpoint['optimizer'])
|
||||
"""
|
||||
# I think it should actually be ok to reload the optimizer before the model.
|
||||
self.dynamic_loss_scale = state_dict['dynamic_loss_scale']
|
||||
self.cur_scale = state_dict['cur_scale']
|
||||
self.cur_iter = state_dict['cur_iter']
|
||||
if state_dict['dynamic_loss_scale']:
|
||||
self.last_overflow_iter = state_dict['last_overflow_iter']
|
||||
self.scale_factor = state_dict['scale_factor']
|
||||
self.scale_window = state_dict['scale_window']
|
||||
self.optimizer.load_state_dict(state_dict['optimizer_state_dict'])
|
||||
# At this point, the optimizer's references to the model's fp32 parameters are up to date.
|
||||
# The optimizer's hyperparameters and internal buffers are also up to date.
|
||||
# However, the fp32 master copies of the model's fp16 params stored by the optimizer are still
|
||||
# out of date. There are two options.
|
||||
# 1: Refresh the master params from the model's fp16 params.
|
||||
# This requires less storage but incurs precision loss.
|
||||
# 2: Save and restore the fp32 master copies separately.
|
||||
# We choose option 2.
|
||||
#
|
||||
# Pytorch Optimizer.load_state_dict casts saved buffers (e.g. momentum) to the type and device
|
||||
# of their associated parameters, because it's possible those buffers might not exist yet in
|
||||
# the current optimizer instance. In our case, as long as the current FP16_Optimizer has been
|
||||
# constructed in the same way as the one whose state_dict we are loading, the same master params
|
||||
# are guaranteed to exist, so we can just copy_() from the saved master params.
|
||||
for current, saved in zip(self.fp32_groups_flat, state_dict['fp32_groups_flat']):
|
||||
current.data.copy_(saved.data)
|
||||
@@ -0,0 +1,390 @@
|
||||
# coding=utf-8
|
||||
# Copyright 2018 The Google AI Language Team Authors and The HugginFace Inc. team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Tokenization classes."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import collections
|
||||
import unicodedata
|
||||
import os
|
||||
import logging
|
||||
|
||||
from .file_utils import cached_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PRETRAINED_VOCAB_ARCHIVE_MAP = {
|
||||
'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-vocab.txt",
|
||||
'bert-large-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased-vocab.txt",
|
||||
'bert-base-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-cased-vocab.txt",
|
||||
'bert-large-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased-vocab.txt",
|
||||
'bert-base-multilingual-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-uncased-vocab.txt",
|
||||
'bert-base-multilingual-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-cased-vocab.txt",
|
||||
'bert-base-chinese': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese-vocab.txt",
|
||||
}
|
||||
PRETRAINED_VOCAB_POSITIONAL_EMBEDDINGS_SIZE_MAP = {
|
||||
'bert-base-uncased': 512,
|
||||
'bert-large-uncased': 512,
|
||||
'bert-base-cased': 512,
|
||||
'bert-large-cased': 512,
|
||||
'bert-base-multilingual-uncased': 512,
|
||||
'bert-base-multilingual-cased': 512,
|
||||
'bert-base-chinese': 512,
|
||||
}
|
||||
VOCAB_NAME = 'vocab.txt'
|
||||
|
||||
|
||||
def load_vocab(vocab_file):
|
||||
"""Loads a vocabulary file into a dictionary."""
|
||||
# mapping unused tokens to special tokens
|
||||
extra_map = {}
|
||||
extra_map['[unused1]'] = '[X_SEP]'
|
||||
for i in range(10):
|
||||
extra_map['[unused{}]'.format(i+2)] = '[SEP_{}]'.format(i)
|
||||
extra_map['[unused12]'] = '[S2S_SEP]'
|
||||
extra_map['[unused13]'] = '[S2S_CLS]'
|
||||
extra_map['[unused14]'] = '[L2R_SEP]'
|
||||
extra_map['[unused15]'] = '[L2R_CLS]'
|
||||
extra_map['[unused16]'] = '[R2L_SEP]'
|
||||
extra_map['[unused17]'] = '[R2L_CLS]'
|
||||
extra_map['[unused18]'] = '[S2S_SOS]'
|
||||
|
||||
vocab = collections.OrderedDict()
|
||||
index = 0
|
||||
with open(vocab_file, "r", encoding="utf-8") as reader:
|
||||
while True:
|
||||
token = reader.readline()
|
||||
if not token:
|
||||
break
|
||||
token = token.strip()
|
||||
if token in extra_map:
|
||||
token = extra_map[token]
|
||||
vocab[token] = index
|
||||
index += 1
|
||||
return vocab
|
||||
|
||||
|
||||
def whitespace_tokenize(text):
|
||||
"""Runs basic whitespace cleaning and splitting on a peice of text."""
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return []
|
||||
tokens = text.split()
|
||||
return tokens
|
||||
|
||||
|
||||
class BertTokenizer(object):
|
||||
"""Runs end-to-end tokenization: punctuation splitting + wordpiece"""
|
||||
|
||||
def __init__(self, vocab_file, do_lower_case=True, max_len=None, never_split=("[UNK]", "[SEP]", "[X_SEP]", "[PAD]", "[CLS]", "[MASK]")):
|
||||
if not os.path.isfile(vocab_file):
|
||||
raise ValueError(
|
||||
"Can't find a vocabulary file at path '{}'. To load the vocabulary from a Google pretrained "
|
||||
"model use `tokenizer = BertTokenizer.from_pretrained(PRETRAINED_MODEL_NAME)`".format(vocab_file))
|
||||
self.vocab = load_vocab(vocab_file)
|
||||
self.ids_to_tokens = collections.OrderedDict(
|
||||
[(ids, tok) for tok, ids in self.vocab.items()])
|
||||
self.basic_tokenizer = BasicTokenizer(
|
||||
do_lower_case=do_lower_case, never_split=never_split)
|
||||
self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab)
|
||||
self.max_len = max_len if max_len is not None else int(1e12)
|
||||
|
||||
def tokenize(self, text):
|
||||
split_tokens = []
|
||||
for token in self.basic_tokenizer.tokenize(text):
|
||||
for sub_token in self.wordpiece_tokenizer.tokenize(token):
|
||||
split_tokens.append(sub_token)
|
||||
return split_tokens
|
||||
|
||||
def convert_tokens_to_ids(self, tokens):
|
||||
"""Converts a sequence of tokens into ids using the vocab."""
|
||||
ids = []
|
||||
for token in tokens:
|
||||
ids.append(self.vocab[token])
|
||||
if len(ids) > self.max_len:
|
||||
raise ValueError(
|
||||
"Token indices sequence length is longer than the specified maximum "
|
||||
" sequence length for this BERT model ({} > {}). Running this"
|
||||
" sequence through BERT will result in indexing errors".format(
|
||||
len(ids), self.max_len)
|
||||
)
|
||||
return ids
|
||||
|
||||
def convert_ids_to_tokens(self, ids):
|
||||
"""Converts a sequence of ids in wordpiece tokens using the vocab."""
|
||||
tokens = []
|
||||
for i in ids:
|
||||
tokens.append(self.ids_to_tokens[i])
|
||||
return tokens
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, pretrained_model_name, cache_dir=None, *inputs, **kwargs):
|
||||
"""
|
||||
Instantiate a PreTrainedBertModel from a pre-trained model file.
|
||||
Download and cache the pre-trained model file if needed.
|
||||
"""
|
||||
if pretrained_model_name in PRETRAINED_VOCAB_ARCHIVE_MAP:
|
||||
vocab_file = PRETRAINED_VOCAB_ARCHIVE_MAP[pretrained_model_name]
|
||||
else:
|
||||
vocab_file = pretrained_model_name
|
||||
if os.path.isdir(vocab_file):
|
||||
vocab_file = os.path.join(vocab_file, VOCAB_NAME)
|
||||
# redirect to the cache, if necessary
|
||||
try:
|
||||
resolved_vocab_file = cached_path(vocab_file, cache_dir=cache_dir)
|
||||
except FileNotFoundError:
|
||||
logger.error(
|
||||
"Model name '{}' was not found in model name list ({}). "
|
||||
"We assumed '{}' was a path or url but couldn't find any file "
|
||||
"associated to this path or url.".format(
|
||||
pretrained_model_name,
|
||||
', '.join(PRETRAINED_VOCAB_ARCHIVE_MAP.keys()),
|
||||
vocab_file))
|
||||
return None
|
||||
if resolved_vocab_file == vocab_file:
|
||||
logger.info("loading vocabulary file {}".format(vocab_file))
|
||||
else:
|
||||
logger.info("loading vocabulary file {} from cache at {}".format(
|
||||
vocab_file, resolved_vocab_file))
|
||||
if pretrained_model_name in PRETRAINED_VOCAB_POSITIONAL_EMBEDDINGS_SIZE_MAP:
|
||||
# if we're using a pretrained model, ensure the tokenizer wont index sequences longer
|
||||
# than the number of positional embeddings
|
||||
max_len = PRETRAINED_VOCAB_POSITIONAL_EMBEDDINGS_SIZE_MAP[pretrained_model_name]
|
||||
kwargs['max_len'] = min(kwargs.get('max_len', int(1e12)), max_len)
|
||||
# Instantiate tokenizer.
|
||||
tokenizer = cls(resolved_vocab_file, *inputs, **kwargs)
|
||||
return tokenizer
|
||||
|
||||
|
||||
class WhitespaceTokenizer(object):
|
||||
def tokenize(self, text):
|
||||
return whitespace_tokenize(text)
|
||||
|
||||
|
||||
class BasicTokenizer(object):
|
||||
"""Runs basic tokenization (punctuation splitting, lower casing, etc.)."""
|
||||
|
||||
def __init__(self, do_lower_case=True, never_split=("[UNK]", "[SEP]", "[PAD]", "[CLS]", "[MASK]")):
|
||||
"""Constructs a BasicTokenizer.
|
||||
|
||||
Args:
|
||||
do_lower_case: Whether to lower case the input.
|
||||
"""
|
||||
self.do_lower_case = do_lower_case
|
||||
self.never_split = never_split
|
||||
|
||||
def tokenize(self, text):
|
||||
"""Tokenizes a piece of text."""
|
||||
text = self._clean_text(text)
|
||||
# This was added on November 1st, 2018 for the multilingual and Chinese
|
||||
# models. This is also applied to the English models now, but it doesn't
|
||||
# matter since the English models were not trained on any Chinese data
|
||||
# and generally don't have any Chinese data in them (there are Chinese
|
||||
# characters in the vocabulary because Wikipedia does have some Chinese
|
||||
# words in the English Wikipedia.).
|
||||
text = self._tokenize_chinese_chars(text)
|
||||
orig_tokens = whitespace_tokenize(text)
|
||||
split_tokens = []
|
||||
for token in orig_tokens:
|
||||
if self.do_lower_case and token not in self.never_split:
|
||||
token = token.lower()
|
||||
token = self._run_strip_accents(token)
|
||||
split_tokens.extend(self._run_split_on_punc(token))
|
||||
|
||||
output_tokens = whitespace_tokenize(" ".join(split_tokens))
|
||||
return output_tokens
|
||||
|
||||
def _run_strip_accents(self, text):
|
||||
"""Strips accents from a piece of text."""
|
||||
text = unicodedata.normalize("NFD", text)
|
||||
output = []
|
||||
for char in text:
|
||||
cat = unicodedata.category(char)
|
||||
if cat == "Mn":
|
||||
continue
|
||||
output.append(char)
|
||||
return "".join(output)
|
||||
|
||||
def _run_split_on_punc(self, text):
|
||||
"""Splits punctuation on a piece of text."""
|
||||
if text in self.never_split:
|
||||
return [text]
|
||||
chars = list(text)
|
||||
i = 0
|
||||
start_new_word = True
|
||||
output = []
|
||||
while i < len(chars):
|
||||
char = chars[i]
|
||||
if _is_punctuation(char):
|
||||
output.append([char])
|
||||
start_new_word = True
|
||||
else:
|
||||
if start_new_word:
|
||||
output.append([])
|
||||
start_new_word = False
|
||||
output[-1].append(char)
|
||||
i += 1
|
||||
|
||||
return ["".join(x) for x in output]
|
||||
|
||||
def _tokenize_chinese_chars(self, text):
|
||||
"""Adds whitespace around any CJK character."""
|
||||
output = []
|
||||
for char in text:
|
||||
cp = ord(char)
|
||||
if self._is_chinese_char(cp):
|
||||
output.append(" ")
|
||||
output.append(char)
|
||||
output.append(" ")
|
||||
else:
|
||||
output.append(char)
|
||||
return "".join(output)
|
||||
|
||||
def _is_chinese_char(self, cp):
|
||||
"""Checks whether CP is the codepoint of a CJK character."""
|
||||
# This defines a "chinese character" as anything in the CJK Unicode block:
|
||||
# https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)
|
||||
#
|
||||
# Note that the CJK Unicode block is NOT all Japanese and Korean characters,
|
||||
# despite its name. The modern Korean Hangul alphabet is a different block,
|
||||
# as is Japanese Hiragana and Katakana. Those alphabets are used to write
|
||||
# space-separated words, so they are not treated specially and handled
|
||||
# like the all of the other languages.
|
||||
if ((cp >= 0x4E00 and cp <= 0x9FFF) or #
|
||||
(cp >= 0x3400 and cp <= 0x4DBF) or #
|
||||
(cp >= 0x20000 and cp <= 0x2A6DF) or #
|
||||
(cp >= 0x2A700 and cp <= 0x2B73F) or #
|
||||
(cp >= 0x2B740 and cp <= 0x2B81F) or #
|
||||
(cp >= 0x2B820 and cp <= 0x2CEAF) or
|
||||
(cp >= 0xF900 and cp <= 0xFAFF) or #
|
||||
(cp >= 0x2F800 and cp <= 0x2FA1F)): #
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _clean_text(self, text):
|
||||
"""Performs invalid character removal and whitespace cleanup on text."""
|
||||
output = []
|
||||
for char in text:
|
||||
cp = ord(char)
|
||||
if cp == 0 or cp == 0xfffd or _is_control(char):
|
||||
continue
|
||||
if _is_whitespace(char):
|
||||
output.append(" ")
|
||||
else:
|
||||
output.append(char)
|
||||
return "".join(output)
|
||||
|
||||
|
||||
class WordpieceTokenizer(object):
|
||||
"""Runs WordPiece tokenization."""
|
||||
|
||||
def __init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=100):
|
||||
self.vocab = vocab
|
||||
self.unk_token = unk_token
|
||||
self.max_input_chars_per_word = max_input_chars_per_word
|
||||
|
||||
def tokenize(self, text):
|
||||
"""Tokenizes a piece of text into its word pieces.
|
||||
|
||||
This uses a greedy longest-match-first algorithm to perform tokenization
|
||||
using the given vocabulary.
|
||||
|
||||
For example:
|
||||
input = "unaffable"
|
||||
output = ["un", "##aff", "##able"]
|
||||
|
||||
Args:
|
||||
text: A single token or whitespace separated tokens. This should have
|
||||
already been passed through `BasicTokenizer`.
|
||||
|
||||
Returns:
|
||||
A list of wordpiece tokens.
|
||||
"""
|
||||
|
||||
output_tokens = []
|
||||
for token in whitespace_tokenize(text):
|
||||
chars = list(token)
|
||||
if len(chars) > self.max_input_chars_per_word:
|
||||
output_tokens.append(self.unk_token)
|
||||
continue
|
||||
|
||||
is_bad = False
|
||||
start = 0
|
||||
sub_tokens = []
|
||||
while start < len(chars):
|
||||
end = len(chars)
|
||||
cur_substr = None
|
||||
while start < end:
|
||||
substr = "".join(chars[start:end])
|
||||
if start > 0:
|
||||
substr = "##" + substr
|
||||
if substr in self.vocab:
|
||||
cur_substr = substr
|
||||
break
|
||||
end -= 1
|
||||
if cur_substr is None:
|
||||
is_bad = True
|
||||
break
|
||||
sub_tokens.append(cur_substr)
|
||||
start = end
|
||||
|
||||
if is_bad:
|
||||
output_tokens.append(self.unk_token)
|
||||
else:
|
||||
output_tokens.extend(sub_tokens)
|
||||
return output_tokens
|
||||
|
||||
|
||||
def _is_whitespace(char):
|
||||
"""Checks whether `chars` is a whitespace character."""
|
||||
# \t, \n, and \r are technically contorl characters but we treat them
|
||||
# as whitespace since they are generally considered as such.
|
||||
if char == " " or char == "\t" or char == "\n" or char == "\r":
|
||||
return True
|
||||
cat = unicodedata.category(char)
|
||||
if cat == "Zs":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_control(char):
|
||||
"""Checks whether `chars` is a control character."""
|
||||
# These are technically control characters but we count them as whitespace
|
||||
# characters.
|
||||
if char == "\t" or char == "\n" or char == "\r":
|
||||
return False
|
||||
cat = unicodedata.category(char)
|
||||
if cat.startswith("C"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_punctuation(char):
|
||||
"""Checks whether `chars` is a punctuation character."""
|
||||
cp = ord(char)
|
||||
# We treat all non-letter/number ASCII as punctuation.
|
||||
# Characters such as "^", "$", and "`" are not in the Unicode
|
||||
# Punctuation class but we treat them as punctuation anyways, for
|
||||
# consistency.
|
||||
if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or
|
||||
(cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)):
|
||||
return True
|
||||
cat = unicodedata.category(char)
|
||||
if cat.startswith("P"):
|
||||
return True
|
||||
return False
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python
|
||||
__author__ = 'xinya'
|
||||
|
||||
from bleu.bleu import Bleu
|
||||
from meteor.meteor import Meteor
|
||||
from rouge.rouge import Rouge
|
||||
from cider.cider import Cider
|
||||
from collections import defaultdict
|
||||
from argparse import ArgumentParser
|
||||
import string
|
||||
|
||||
import sys
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf-8')
|
||||
|
||||
_tok_dict = {"(": "-lrb-", ")": "-rrb-",
|
||||
"[": "-lsb-", "]": "-rsb-",
|
||||
"{": "-lcb-", "}": "-rcb-",
|
||||
"[UNK]": "UNK", '&': '&', '<': '<', '>': '>'}
|
||||
|
||||
|
||||
def _is_digit(w):
|
||||
for ch in w:
|
||||
if not(ch.isdigit() or ch == ','):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def detokenize(tk_list):
|
||||
r_list = []
|
||||
for tk in tk_list:
|
||||
if tk.startswith('##') and len(r_list) > 0:
|
||||
r_list[-1] = r_list[-1] + tk[2:]
|
||||
else:
|
||||
r_list.append(tk)
|
||||
return r_list
|
||||
|
||||
|
||||
def fix_tokenization(text):
|
||||
input_tokens = text.split()
|
||||
output_tokens = []
|
||||
has_left_quote = False
|
||||
has_left_single_quote = False
|
||||
|
||||
i = 0
|
||||
prev_dash = False
|
||||
while i < len(input_tokens):
|
||||
tok = input_tokens[i]
|
||||
flag_prev_dash = False
|
||||
if tok in _tok_dict.keys():
|
||||
output_tokens.append(_tok_dict[tok])
|
||||
i += 1
|
||||
elif tok == "\"":
|
||||
if has_left_quote:
|
||||
output_tokens.append("''")
|
||||
else:
|
||||
output_tokens.append("``")
|
||||
has_left_quote = not has_left_quote
|
||||
i += 1
|
||||
elif tok == "'" and len(output_tokens) > 0 and output_tokens[-1].endswith("n") and i < len(input_tokens) - 1 and input_tokens[i + 1] == "t":
|
||||
output_tokens[-1] = output_tokens[-1][:-1]
|
||||
output_tokens.append("n't")
|
||||
i += 2
|
||||
elif tok == "'" and i < len(input_tokens) - 1 and input_tokens[i + 1] in ("s", "d", "ll"):
|
||||
output_tokens.append("'"+input_tokens[i + 1])
|
||||
i += 2
|
||||
elif tok == "'":
|
||||
if has_left_single_quote:
|
||||
output_tokens.append("'")
|
||||
else:
|
||||
output_tokens.append("`")
|
||||
has_left_single_quote = not has_left_single_quote
|
||||
i += 1
|
||||
elif tok == "." and i < len(input_tokens) - 2 and input_tokens[i + 1] == "." and input_tokens[i + 2] == ".":
|
||||
output_tokens.append("...")
|
||||
i += 3
|
||||
elif tok == "," and len(output_tokens) > 0 and _is_digit(output_tokens[-1]) and i < len(input_tokens) - 1 and _is_digit(input_tokens[i + 1]):
|
||||
# $ 3 , 000 -> $ 3,000
|
||||
output_tokens[-1] += ','+input_tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "." and len(output_tokens) > 0 and output_tokens[-1].isdigit() and i < len(input_tokens) - 1 and input_tokens[i + 1].isdigit():
|
||||
# 3 . 03 -> $ 3.03
|
||||
output_tokens[-1] += '.'+input_tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "." and len(output_tokens) > 0 and len(output_tokens[-1]) == 1 and output_tokens[-1].isupper() and i < len(input_tokens) - 2 and len(input_tokens[i + 1]) == 1 and input_tokens[i + 1].isupper() and input_tokens[i + 2] == '.':
|
||||
# U . N . -> U.N.
|
||||
k = i+3
|
||||
while k+2 < len(input_tokens):
|
||||
if len(input_tokens[k + 1]) == 1 and input_tokens[k + 1].isupper() and input_tokens[k + 2] == '.':
|
||||
k += 2
|
||||
else:
|
||||
break
|
||||
output_tokens[-1] += ''.join(input_tokens[i:k])
|
||||
i += 2
|
||||
elif tok == "-":
|
||||
if i < len(input_tokens) - 1 and input_tokens[i + 1] == "-":
|
||||
output_tokens.append("--")
|
||||
i += 2
|
||||
elif i == len(input_tokens) - 1 or i == 0:
|
||||
output_tokens.append("-")
|
||||
i += 1
|
||||
elif output_tokens[-1] not in string.punctuation and input_tokens[i + 1][0] not in string.punctuation:
|
||||
output_tokens[-1] += "-"
|
||||
i += 1
|
||||
flag_prev_dash = True
|
||||
else:
|
||||
output_tokens.append("-")
|
||||
i += 1
|
||||
elif prev_dash and len(output_tokens) > 0 and tok[0] not in string.punctuation:
|
||||
output_tokens[-1] += tok
|
||||
i += 1
|
||||
else:
|
||||
output_tokens.append(tok)
|
||||
i += 1
|
||||
prev_dash = flag_prev_dash
|
||||
return " ".join(output_tokens)
|
||||
|
||||
|
||||
class QGEvalCap:
|
||||
def __init__(self, gts, res):
|
||||
self.gts = gts
|
||||
self.res = res
|
||||
|
||||
def evaluate(self):
|
||||
output = []
|
||||
scorers = [
|
||||
(Bleu(4), ["Bleu_1", "Bleu_2", "Bleu_3", "Bleu_4"]),
|
||||
(Meteor(), "METEOR"),
|
||||
(Rouge(), "ROUGE_L"),
|
||||
# (Cider(), "CIDEr")
|
||||
]
|
||||
|
||||
# =================================================
|
||||
# Compute scores
|
||||
# =================================================
|
||||
for scorer, method in scorers:
|
||||
# print 'computing %s score...'%(scorer.method())
|
||||
score, scores = scorer.compute_score(self.gts, self.res)
|
||||
if type(method) == list:
|
||||
for sc, scs, m in zip(score, scores, method):
|
||||
print "%s: %0.5f" % (m, sc)
|
||||
output.append(sc)
|
||||
else:
|
||||
print "%s: %0.5f" % (method, score)
|
||||
output.append(score)
|
||||
return output
|
||||
|
||||
|
||||
def eval(out_file, src_file, tgt_file, isDIn=False, num_pairs=500):
|
||||
"""
|
||||
Given a filename, calculate the metric scores for that prediction file
|
||||
|
||||
isDin: boolean value to check whether input file is DirectIn.txt
|
||||
"""
|
||||
|
||||
pairs = []
|
||||
with open(src_file, 'r') as infile:
|
||||
for line in infile:
|
||||
pair = {}
|
||||
pair['tokenized_sentence'] = line[:-1].strip().lower()
|
||||
pairs.append(pair)
|
||||
|
||||
with open(tgt_file, "r") as infile:
|
||||
cnt = 0
|
||||
for line in infile:
|
||||
pairs[cnt]['tokenized_question'] = line[:-1].strip()
|
||||
cnt += 1
|
||||
|
||||
output = []
|
||||
with open(out_file, 'r') as infile:
|
||||
for line in infile:
|
||||
line = fix_tokenization(line[:-1].strip()).lower()
|
||||
output.append(line)
|
||||
|
||||
for idx, pair in enumerate(pairs):
|
||||
pair['prediction'] = output[idx]
|
||||
|
||||
# eval
|
||||
from eval import QGEvalCap
|
||||
import json
|
||||
from json import encoder
|
||||
encoder.FLOAT_REPR = lambda o: format(o, '.4f')
|
||||
|
||||
res = defaultdict(lambda: [])
|
||||
gts = defaultdict(lambda: [])
|
||||
|
||||
for pair in pairs[:]:
|
||||
key = pair['tokenized_sentence']
|
||||
res[key] = [pair['prediction'].encode('utf-8')]
|
||||
|
||||
# gts
|
||||
gts[key].append(pair['tokenized_question'].encode('utf-8'))
|
||||
|
||||
QGEval = QGEvalCap(gts, res)
|
||||
return QGEval.evaluate()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("-out", "--out_file", dest="out_file",
|
||||
default="./output/pred.txt", help="output file to compare")
|
||||
parser.add_argument("-src", "--src_file", dest="src_file",
|
||||
default="./qg_data/test/test.pa.txt", help="src file")
|
||||
parser.add_argument("-tgt", "--tgt_file", dest="tgt_file",
|
||||
default="./qg_data/nqg_processed_data/tgt-test.txt", help="target file")
|
||||
args = parser.parse_args()
|
||||
|
||||
print "scores: \n"
|
||||
eval(args.out_file, args.src_file, args.tgt_file)
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python
|
||||
__author__ = 'xinya'
|
||||
|
||||
from bleu.bleu import Bleu
|
||||
from meteor.meteor import Meteor
|
||||
from rouge.rouge import Rouge
|
||||
from cider.cider import Cider
|
||||
from collections import defaultdict
|
||||
from argparse import ArgumentParser
|
||||
import string
|
||||
|
||||
import sys
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf-8')
|
||||
|
||||
_tok_dict = {"(": "-lrb-", ")": "-rrb-",
|
||||
"[": "-lsb-", "]": "-rsb-",
|
||||
"{": "-lcb-", "}": "-rcb-",
|
||||
"[UNK]": "UNK", '&': '&', '<': '<', '>': '>'}
|
||||
|
||||
|
||||
def _is_digit(w):
|
||||
for ch in w:
|
||||
if not(ch.isdigit() or ch == ','):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def detokenize(tk_list):
|
||||
r_list = []
|
||||
for tk in tk_list:
|
||||
if tk.startswith('##') and len(r_list) > 0:
|
||||
r_list[-1] = r_list[-1] + tk[2:]
|
||||
else:
|
||||
r_list.append(tk)
|
||||
return r_list
|
||||
|
||||
|
||||
def fix_tokenization(text):
|
||||
input_tokens = text.split()
|
||||
output_tokens = []
|
||||
has_left_quote = False
|
||||
has_left_single_quote = False
|
||||
|
||||
i = 0
|
||||
prev_dash = False
|
||||
while i < len(input_tokens):
|
||||
tok = input_tokens[i]
|
||||
flag_prev_dash = False
|
||||
if tok in _tok_dict.keys():
|
||||
output_tokens.append(_tok_dict[tok])
|
||||
i += 1
|
||||
elif tok == "\"":
|
||||
if has_left_quote:
|
||||
output_tokens.append("''")
|
||||
else:
|
||||
output_tokens.append("``")
|
||||
has_left_quote = not has_left_quote
|
||||
i += 1
|
||||
elif tok == "'" and len(output_tokens) > 0 and output_tokens[-1].endswith("n") and i < len(input_tokens) - 1 and input_tokens[i + 1] == "t":
|
||||
output_tokens[-1] = output_tokens[-1][:-1]
|
||||
output_tokens.append("n't")
|
||||
i += 2
|
||||
elif tok == "'" and i < len(input_tokens) - 1 and input_tokens[i + 1] in ("s", "d", "ll"):
|
||||
output_tokens.append("'"+input_tokens[i + 1])
|
||||
i += 2
|
||||
elif tok == "'":
|
||||
if has_left_single_quote:
|
||||
output_tokens.append("'")
|
||||
else:
|
||||
output_tokens.append("`")
|
||||
has_left_single_quote = not has_left_single_quote
|
||||
i += 1
|
||||
elif tok == "." and i < len(input_tokens) - 2 and input_tokens[i + 1] == "." and input_tokens[i + 2] == ".":
|
||||
output_tokens.append("...")
|
||||
i += 3
|
||||
elif tok == "," and len(output_tokens) > 0 and _is_digit(output_tokens[-1]) and i < len(input_tokens) - 1 and _is_digit(input_tokens[i + 1]):
|
||||
# $ 3 , 000 -> $ 3,000
|
||||
output_tokens[-1] += ','+input_tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "." and len(output_tokens) > 0 and output_tokens[-1].isdigit() and i < len(input_tokens) - 1 and input_tokens[i + 1].isdigit():
|
||||
# 3 . 03 -> $ 3.03
|
||||
output_tokens[-1] += '.'+input_tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "." and len(output_tokens) > 0 and len(output_tokens[-1]) == 1 and output_tokens[-1].isupper() and i < len(input_tokens) - 2 and len(input_tokens[i + 1]) == 1 and input_tokens[i + 1].isupper() and input_tokens[i + 2] == '.':
|
||||
# U . N . -> U.N.
|
||||
k = i+3
|
||||
while k+2 < len(input_tokens):
|
||||
if len(input_tokens[k + 1]) == 1 and input_tokens[k + 1].isupper() and input_tokens[k + 2] == '.':
|
||||
k += 2
|
||||
else:
|
||||
break
|
||||
output_tokens[-1] += ''.join(input_tokens[i:k])
|
||||
i += 2
|
||||
elif tok == "-":
|
||||
if i < len(input_tokens) - 1 and input_tokens[i + 1] == "-":
|
||||
output_tokens.append("--")
|
||||
i += 2
|
||||
elif i == len(input_tokens) - 1 or i == 0:
|
||||
output_tokens.append("-")
|
||||
i += 1
|
||||
elif output_tokens[-1] not in string.punctuation and input_tokens[i + 1][0] not in string.punctuation:
|
||||
output_tokens[-1] += "-"
|
||||
i += 1
|
||||
flag_prev_dash = True
|
||||
else:
|
||||
output_tokens.append("-")
|
||||
i += 1
|
||||
elif prev_dash and len(output_tokens) > 0 and tok[0] not in string.punctuation:
|
||||
output_tokens[-1] += tok
|
||||
i += 1
|
||||
else:
|
||||
output_tokens.append(tok)
|
||||
i += 1
|
||||
prev_dash = flag_prev_dash
|
||||
return " ".join(output_tokens)
|
||||
|
||||
|
||||
class QGEvalCap:
|
||||
def __init__(self, gts, res):
|
||||
self.gts = gts
|
||||
self.res = res
|
||||
|
||||
def evaluate(self):
|
||||
output = []
|
||||
scorers = [
|
||||
(Bleu(4), ["Bleu_1", "Bleu_2", "Bleu_3", "Bleu_4"]),
|
||||
(Meteor(), "METEOR"),
|
||||
(Rouge(), "ROUGE_L"),
|
||||
# (Cider(), "CIDEr")
|
||||
]
|
||||
|
||||
# =================================================
|
||||
# Compute scores
|
||||
# =================================================
|
||||
for scorer, method in scorers:
|
||||
# print 'computing %s score...'%(scorer.method())
|
||||
score, scores = scorer.compute_score(self.gts, self.res)
|
||||
if type(method) == list:
|
||||
for sc, scs, m in zip(score, scores, method):
|
||||
print "%s: %0.5f" % (m, sc)
|
||||
output.append(sc)
|
||||
else:
|
||||
print "%s: %0.5f" % (method, score)
|
||||
output.append(score)
|
||||
return output
|
||||
|
||||
|
||||
def eval(out_file, src_file, tgt_file, isDIn=False, num_pairs=500):
|
||||
"""
|
||||
Given a filename, calculate the metric scores for that prediction file
|
||||
|
||||
isDin: boolean value to check whether input file is DirectIn.txt
|
||||
"""
|
||||
|
||||
pairs = []
|
||||
with open(src_file, 'r') as infile:
|
||||
for line in infile:
|
||||
pair = {}
|
||||
pair['tokenized_sentence'] = line[:-1].strip().lower()
|
||||
pairs.append(pair)
|
||||
|
||||
with open(tgt_file, "r") as infile:
|
||||
cnt = 0
|
||||
for line in infile:
|
||||
pairs[cnt]['tokenized_question'] = " ".join(
|
||||
detokenize(line[:-1].strip().split())).lower()
|
||||
cnt += 1
|
||||
|
||||
output = []
|
||||
with open(out_file, 'r') as infile:
|
||||
for line in infile:
|
||||
line = line[:-1].strip().lower()
|
||||
output.append(line)
|
||||
|
||||
for idx, pair in enumerate(pairs):
|
||||
pair['prediction'] = output[idx]
|
||||
|
||||
# eval
|
||||
from eval import QGEvalCap
|
||||
import json
|
||||
from json import encoder
|
||||
encoder.FLOAT_REPR = lambda o: format(o, '.4f')
|
||||
|
||||
res = defaultdict(lambda: [])
|
||||
gts = defaultdict(lambda: [])
|
||||
|
||||
for pair in pairs[:]:
|
||||
key = pair['tokenized_sentence']
|
||||
res[key] = [pair['prediction'].encode('utf-8')]
|
||||
|
||||
# gts
|
||||
gts[key].append(pair['tokenized_question'].encode('utf-8'))
|
||||
|
||||
QGEval = QGEvalCap(gts, res)
|
||||
return QGEval.evaluate()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("-out", "--out_file", dest="out_file",
|
||||
default="./output/pred.txt", help="output file to compare")
|
||||
parser.add_argument("-src", "--src_file", dest="src_file",
|
||||
default="./qg_data/test/test.pa.txt", help="src file")
|
||||
parser.add_argument("-tgt", "--tgt_file", dest="tgt_file",
|
||||
default="./qg_data/test/test.q.tok.txt", help="target file")
|
||||
args = parser.parse_args()
|
||||
|
||||
print "scores: \n"
|
||||
eval(args.out_file, args.src_file, args.tgt_file)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Simple check list from AllenNLP repo: https://github.com/allenai/allennlp/blob/master/setup.py
|
||||
|
||||
To create the package for pypi.
|
||||
|
||||
1. Change the version in __init__.py and setup.py.
|
||||
|
||||
2. Commit these changes with the message: "Release: VERSION"
|
||||
|
||||
3. Add a tag in git to mark the release: "git tag VERSION -m'Adds tag VERSION for pypi' "
|
||||
Push the tag to git: git push --tags origin master
|
||||
|
||||
4. Build both the sources and the wheel. Do not change anything in setup.py between
|
||||
creating the wheel and the source distribution (obviously).
|
||||
|
||||
For the wheel, run: "python setup.py bdist_wheel" in the top level allennlp directory.
|
||||
(this will build a wheel for the python version you use to build it - make sure you use python 3.x).
|
||||
|
||||
For the sources, run: "python setup.py sdist"
|
||||
You should now have a /dist directory with both .whl and .tar.gz source versions of allennlp.
|
||||
|
||||
5. Check that everything looks correct by uploading the package to the pypi test server:
|
||||
|
||||
twine upload dist/* -r pypitest
|
||||
(pypi suggest using twine as other methods upload files via plaintext.)
|
||||
|
||||
Check that you can install it in a virtualenv by running:
|
||||
pip install -i https://testpypi.python.org/pypi allennlp
|
||||
|
||||
6. Upload the final version to actual pypi:
|
||||
twine upload dist/* -r pypi
|
||||
|
||||
7. Copy the release notes from RELEASE.md to the tag in github once everything is looking hunky-dory.
|
||||
|
||||
"""
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="pytorch_pretrained_bert",
|
||||
version="0.4.0",
|
||||
author="Thomas Wolf, Victor Sanh, Tim Rault, Google AI Language Team Authors",
|
||||
author_email="thomas@huggingface.co",
|
||||
description="PyTorch version of Google AI BERT model with script to load Google pre-trained models",
|
||||
long_description="pytorch",
|
||||
long_description_content_type="text/markdown",
|
||||
keywords='BERT NLP deep learning google',
|
||||
license='Apache',
|
||||
url="https://github.com/huggingface/pytorch-pretrained-BERT",
|
||||
packages=find_packages(exclude=["*.tests", "*.tests.*",
|
||||
"tests.*", "tests"]),
|
||||
install_requires=['numpy',
|
||||
'boto3',
|
||||
'requests',
|
||||
'tqdm'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
"pytorch_pretrained_bert=pytorch_pretrained_bert.__main__:main"
|
||||
]
|
||||
},
|
||||
python_requires='>=3.5.0',
|
||||
tests_require=['pytest'],
|
||||
classifiers=[
|
||||
'Intended Audience :: Science/Research',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Scientific/Engineering :: Artificial Intelligence',
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user