Spaces:
Sleeping
Sleeping
Upload 43 files
Browse files- README.rst +175 -0
- nlp.py +0 -0
- requirements.txt +80 -0
- src/.DS_Store +0 -0
- src/__init__.py +0 -0
- src/__pycache__/__init__.cpython-311.pyc +0 -0
- src/__pycache__/classes.cpython-311.pyc +0 -0
- src/__pycache__/conll_output.cpython-311.pyc +0 -0
- src/__pycache__/data_preparation.cpython-311.pyc +0 -0
- src/__pycache__/logger.cpython-311.pyc +0 -0
- src/classes.py +70 -0
- src/conll_output.py +50 -0
- src/data_preparation.py +199 -0
- src/dependency_parser/__init__.py +0 -0
- src/dependency_parser/__pycache__/__init__.cpython-311.pyc +0 -0
- src/dependency_parser/__pycache__/biaff_parser.cpython-311.pyc +0 -0
- src/dependency_parser/biaff_parser.py +50 -0
- src/initialize_disambiguator/__init__.py +0 -0
- src/initialize_disambiguator/__pycache__/__init__.cpython-311.pyc +0 -0
- src/initialize_disambiguator/__pycache__/bert_disambiguator.cpython-311.pyc +0 -0
- src/initialize_disambiguator/__pycache__/disambiguator_interface.cpython-311.pyc +0 -0
- src/initialize_disambiguator/__pycache__/mle_disambiguator.cpython-311.pyc +0 -0
- src/initialize_disambiguator/bert_disambiguator.py +11 -0
- src/initialize_disambiguator/disambiguator_interface.py +28 -0
- src/initialize_disambiguator/mle_disambiguator.py +17 -0
- src/logger.py +46 -0
- src/parse_disambiguation/__init__.py +0 -0
- src/parse_disambiguation/__pycache__/__init__.cpython-311.pyc +0 -0
- src/parse_disambiguation/__pycache__/disambiguation_analysis.cpython-311.pyc +0 -0
- src/parse_disambiguation/__pycache__/feature_extraction.cpython-311.pyc +0 -0
- src/parse_disambiguation/disambiguation_analysis.py +69 -0
- src/parse_disambiguation/feature_extraction.py +212 -0
- src/utils/__init__.py +0 -0
- src/utils/__pycache__/__init__.cpython-311.pyc +0 -0
- src/utils/__pycache__/conll_fixes.cpython-311.pyc +0 -0
- src/utils/__pycache__/model_downloader.cpython-311.pyc +0 -0
- src/utils/__pycache__/text_cleaner.cpython-311.pyc +0 -0
- src/utils/conll_fixes.py +33 -0
- src/utils/model_downloader.py +33 -0
- src/utils/text_cleaner.py +34 -0
- tests/test_feats.tsv +3 -0
- tests/test_feature_extraction.py +55 -0
- text_to_conll_cli.py +110 -0
README.rst
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
NLP : - TEXT TO KNOWLEDGE GRAPH
|
| 2 |
+
we use the CamelParser to process it and extract the knowledge graph in JSON format.
|
| 3 |
+
Then we use RULE-BASED Resoning to Extract Entities and Relations from the text and build a knowledge graph in JSON format.
|
| 4 |
+
|
| 5 |
+
CamelParser
|
| 6 |
+
=============
|
| 7 |
+
|
| 8 |
+
.. image:: https://img.shields.io/pypi/l/camel-tools.svg
|
| 9 |
+
:target: https://opensource.org/licenses/MIT
|
| 10 |
+
:alt: MIT License
|
| 11 |
+
|
| 12 |
+
Introduction
|
| 13 |
+
------------
|
| 14 |
+
|
| 15 |
+
CamelParser is an open-source Python-based Arabic dependency parser targeting two popular
|
| 16 |
+
Arabic dependency formalisms, the Columbia Arabic Treebank (CATiB), and Universal Dependencies (UD).
|
| 17 |
+
|
| 18 |
+
The CamelParser pipeline handles the processing of raw text and produces tokenization,
|
| 19 |
+
part-of-speech and rich morphological features. For disambiguation, users can choose between
|
| 20 |
+
the BERT unfactored disambiguator, or a lighter Maximum Likelihood Estimation (MLE) disambiguator,
|
| 21 |
+
both of which are included in CAMeL Tools. For dependency parsing, we use the SuPar Biaffine Dependency Parser.
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
Installation
|
| 25 |
+
------------
|
| 26 |
+
1. Clone this repo
|
| 27 |
+
|
| 28 |
+
2. Set up a virtual environment using Python 3.11.13 (you can follow the tutorial `here <https://www.youtube.com/watch?si=g2zx-D0zdkL50knb&v=31WU0Dhw4sk&feature=youtu.be>`_).
|
| 29 |
+
|
| 30 |
+
Currently, CamelParser does not work with later versions of python due to issues with some dependencies.
|
| 31 |
+
|
| 32 |
+
3. Install the required packages:
|
| 33 |
+
|
| 34 |
+
.. code-block:: bash
|
| 35 |
+
|
| 36 |
+
pip install -r requirements.txt
|
| 37 |
+
|
| 38 |
+
4. Download dependency parsing models:
|
| 39 |
+
|
| 40 |
+
.. code-block:: bash
|
| 41 |
+
|
| 42 |
+
python download_models.py
|
| 43 |
+
|
| 44 |
+
Currently, two Arabic script models, CATiB and UD, will be downloaded from the CAMeL Lab's
|
| 45 |
+
`parser models collection <https://huggingface.co/collections/CAMeL-Lab/camelparser-654a3df21f70b3b5e72f95d9>`_
|
| 46 |
+
on Hugging Face. More models will be added soon!
|
| 47 |
+
|
| 48 |
+
Examples
|
| 49 |
+
--------
|
| 50 |
+
The CamelParser allows users to pass either a string or a file containing **one or more** sentences.
|
| 51 |
+
Below are examples using the different *string* inputs that CamelParser accepts.
|
| 52 |
+
We pass each example as a string using -s.
|
| 53 |
+
However, we do recommend using the file method (-i) along with the path to the file
|
| 54 |
+
when passing multiple sentences.
|
| 55 |
+
|
| 56 |
+
You can also refer to the **sample_starting_point.py** to use the parser in your code, or for more advanced usage:
|
| 57 |
+
|
| 58 |
+
.. code-block:: bash
|
| 59 |
+
|
| 60 |
+
text_to_conll_cli.py
|
| 61 |
+
|
| 62 |
+
handle_multiple_texts.py
|
| 63 |
+
|
| 64 |
+
handle_multiple_conll_files.py
|
| 65 |
+
|
| 66 |
+
Passing text
|
| 67 |
+
|
| 68 |
+
.. code-block:: bash
|
| 69 |
+
|
| 70 |
+
python text_to_conll_cli.py -f text -s "جامعة نيويورك أبو ظبي تنشر أول أطلس لكوكب المريخ باللغة العربية."
|
| 71 |
+
|
| 72 |
+
The verbose version of the above example (default values are shown)
|
| 73 |
+
|
| 74 |
+
.. code-block:: bash
|
| 75 |
+
|
| 76 |
+
python text_to_conll_cli.py -f text -b r13 -d bert -m catib -s "جامعة نيويورك أبو ظبي تنشر أول أطلس لكوكب المريخ باللغة العربية."
|
| 77 |
+
|
| 78 |
+
Passing preprocessed text (cleaned and whitespace tokenized)
|
| 79 |
+
|
| 80 |
+
.. code-block:: bash
|
| 81 |
+
|
| 82 |
+
python text_to_conll_cli.py -f preprocessed_text -s "جامعة نيويورك أبو ظبي تنشر أول أطلس لكوكب المريخ باللغة العربية ."
|
| 83 |
+
|
| 84 |
+
*Note that the difference between the -f text and preprocessed_text parser input settings is*
|
| 85 |
+
*that for text we use different utilities from CAMeL Tools to*
|
| 86 |
+
`normalize unicode <https://camel-tools.readthedocs.io/en/latest/api/utils/normalize.html?highlight=normalize_unicode#camel_tools.utils.normalize.normalize_unicode>`_,
|
| 87 |
+
`dediactritize <https://camel-tools.readthedocs.io/en/latest/api/utils/dediac.html?highlight=dediac_ar>`_,
|
| 88 |
+
*clean the text using*
|
| 89 |
+
`arclean <https://camel-tools.readthedocs.io/en/latest/api/utils/charmap.html?highlight=arclean#utility>`_,
|
| 90 |
+
*and perform*
|
| 91 |
+
`whitespace tokenization <https://camel-tools.readthedocs.io/en/latest/api/tokenizers/word.html?highlight=simple_word_tokenize#camel_tools.tokenizers.word.simple_word_tokenize>`_.
|
| 92 |
+
|
| 93 |
+
tokenized is used when 1) the text has already been tokenized, and 2) only dependency relations are needed;
|
| 94 |
+
the POS tags and features will not be generated.
|
| 95 |
+
|
| 96 |
+
.. code-block:: bash
|
| 97 |
+
|
| 98 |
+
python text_to_conll_cli.py -f tokenized -s "جامعة نيويورك أبو ظبي تنشر أول أطلس ل+ كوكب المريخ ب+ اللغة العربية ."
|
| 99 |
+
|
| 100 |
+
tokenized_tagged is used when the user has the tokens and POS tags. They should be passed as tuples.
|
| 101 |
+
|
| 102 |
+
.. code-block:: bash
|
| 103 |
+
|
| 104 |
+
python text_to_conll_cli.py -f tokenized_tagged -s "(جامعة, NOM) (نيويورك, PROP) (أبو, PROP) (ظبي, PROP) (تنشر, VRB) (أول, NOM) (أطلس, NOM) (ل+, PRT) (كوكب, NOM) (المريخ, PROP) (ب+, PRT) (اللغة, NOM) (العربية, NOM) (., PNX)"
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
Using a custom model
|
| 108 |
+
------------------
|
| 109 |
+
You can use your own dependency parser models by
|
| 110 |
+
|
| 111 |
+
1. placing the model in the models directory (this directory is created when you run download_models.py, but you can create it yourself)
|
| 112 |
+
|
| 113 |
+
2. place your model in the models directory
|
| 114 |
+
|
| 115 |
+
3. when running one of the scripts, add -m [model_name]. Just type the model name WITHOUT the path.
|
| 116 |
+
|
| 117 |
+
Extending the code
|
| 118 |
+
------------------
|
| 119 |
+
|
| 120 |
+
You can also use different parts of the code to create your own pipeline.
|
| 121 |
+
The handle_multiple_texts.py is an example of that. It can be used to parse a directory of text files,
|
| 122 |
+
saving the resulting CoNLL-X files to a given output directory.
|
| 123 |
+
|
| 124 |
+
Using another morphology database
|
| 125 |
+
---------------------------------
|
| 126 |
+
|
| 127 |
+
Curently, the CamelParser uses CAMeLTools' default morphology database, the morphology-db-msa-r13.
|
| 128 |
+
|
| 129 |
+
For our paper, we used the calima-msa-s31 database. To use this database,
|
| 130 |
+
follow these steps (note that you need an account with the LDC):
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
1. Install camel_tools v1.5.6 or later (you can check this using camel_data -v)
|
| 134 |
+
|
| 135 |
+
2. Download the camel data for the BERT unfactored (MSA) model, as well as the morphology database:
|
| 136 |
+
|
| 137 |
+
.. code-block:: bash
|
| 138 |
+
|
| 139 |
+
camel_data -i morphology-db-msa-s31
|
| 140 |
+
camel_data -i disambig-bert-unfactored-msa
|
| 141 |
+
|
| 142 |
+
3. Download the LDC2010L01 from the ldc downloads:
|
| 143 |
+
- go to https://catalog.ldc.upenn.edu/organization/downloads
|
| 144 |
+
- search for LDC2010L01.tgz and download it
|
| 145 |
+
|
| 146 |
+
4. DO NOT EXTRACT LDC2010L01.tgz! We'll use the following command from camel tools to install the db:
|
| 147 |
+
|
| 148 |
+
.. code-block:: bash
|
| 149 |
+
|
| 150 |
+
camel_data -p morphology-db-msa-s31 /path/to/LDC2010L01.tgz
|
| 151 |
+
|
| 152 |
+
5. When running the main script, use -b and pass calima-msa-s31.
|
| 153 |
+
|
| 154 |
+
Reproducing paper results
|
| 155 |
+
-------------------------
|
| 156 |
+
|
| 157 |
+
To reproduce the results in our paper *CamelParser2.0: A State-of-the-Art Dependency Parser for Arabic*, please use the code found
|
| 158 |
+
in the *paper_version* branch.
|
| 159 |
+
|
| 160 |
+
Citation
|
| 161 |
+
--------
|
| 162 |
+
|
| 163 |
+
If you find the CamelParser useful in your research, please cite
|
| 164 |
+
|
| 165 |
+
.. code-block:: bibtex
|
| 166 |
+
|
| 167 |
+
@inproceedings{Elshabrawy:2023:camelparser,
|
| 168 |
+
title = "{CamelParser2.0: A State-of-the-Art Dependency Parser for Arabic}",
|
| 169 |
+
author = {Ahmed Elshabrawy and
|
| 170 |
+
Muhammed AbuOdeh and
|
| 171 |
+
Go Inoue and
|
| 172 |
+
Nizar Habash} ,
|
| 173 |
+
booktitle = {Proceedings of The First Arabic Natural Language Processing Conference (ArabicNLP 2023)},
|
| 174 |
+
year = "2023"
|
| 175 |
+
}
|
nlp.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
annotated-doc==0.0.5
|
| 2 |
+
antlr4-python3-runtime==4.9.3
|
| 3 |
+
anyio==4.14.2
|
| 4 |
+
cachetools==5.3.1
|
| 5 |
+
camel-kenlm==2025.4.8
|
| 6 |
+
camel_tools==1.5.6
|
| 7 |
+
certifi==2023.5.7
|
| 8 |
+
charset-normalizer==3.1.0
|
| 9 |
+
click==8.1.3
|
| 10 |
+
colorama==0.4.6
|
| 11 |
+
defusedxml==0.7.1
|
| 12 |
+
dill==0.3.7
|
| 13 |
+
docopt==0.6.2
|
| 14 |
+
editdistance==0.6.2
|
| 15 |
+
emoji==2.4.0
|
| 16 |
+
exceptiongroup==1.1.3
|
| 17 |
+
filelock==3.12.0
|
| 18 |
+
fsspec==2023.5.0
|
| 19 |
+
future==0.18.3
|
| 20 |
+
h11==0.16.0
|
| 21 |
+
hf-xet==1.6.0
|
| 22 |
+
httpcore==1.0.9
|
| 23 |
+
httpx==0.28.1
|
| 24 |
+
huggingface-hub==0.15.1
|
| 25 |
+
hydra-core==1.3.2
|
| 26 |
+
idna==3.4
|
| 27 |
+
iniconfig==2.0.0
|
| 28 |
+
Jinja2==3.1.2
|
| 29 |
+
joblib==1.2.0
|
| 30 |
+
markdown-it-py==4.2.0
|
| 31 |
+
MarkupSafe==2.1.3
|
| 32 |
+
mdurl==0.1.2
|
| 33 |
+
mpmath==1.3.0
|
| 34 |
+
muddler==0.1.3
|
| 35 |
+
multiprocess==0.70.15
|
| 36 |
+
narwhals==2.24.0
|
| 37 |
+
networkx==3.1
|
| 38 |
+
nltk==3.8.1
|
| 39 |
+
numpy==1.24.3
|
| 40 |
+
omegaconf==2.3.0
|
| 41 |
+
opt-einsum==3.3.0
|
| 42 |
+
packaging==23.1
|
| 43 |
+
pandas==2.0.2
|
| 44 |
+
pathos==0.3.1
|
| 45 |
+
platformdirs==4.11.1
|
| 46 |
+
pluggy==1.2.0
|
| 47 |
+
pox==0.3.3
|
| 48 |
+
ppft==1.7.6.7
|
| 49 |
+
protobuf==4.23.2
|
| 50 |
+
Pygments==2.20.0
|
| 51 |
+
pyrsistent==0.19.3
|
| 52 |
+
pytest==7.4.0
|
| 53 |
+
python-dateutil==2.8.2
|
| 54 |
+
pytz==2023.3
|
| 55 |
+
PyYAML==6.0
|
| 56 |
+
regex==2023.6.3
|
| 57 |
+
requests==2.31.0
|
| 58 |
+
rich==15.0.0
|
| 59 |
+
safetensors==0.8.0
|
| 60 |
+
scikit-learn==1.2.2
|
| 61 |
+
scipy==1.10.1
|
| 62 |
+
shellingham==1.5.4
|
| 63 |
+
six==1.16.0
|
| 64 |
+
stanza==1.5.0
|
| 65 |
+
supar @ git+https://github.com/yzhangcs/parser.git@17ec77dd05aaaa8160e3555134ff5f3b654e4991
|
| 66 |
+
sympy==1.12
|
| 67 |
+
tabulate==0.9.0
|
| 68 |
+
termcolor==3.3.0
|
| 69 |
+
threadpoolctl==3.1.0
|
| 70 |
+
tokenizers==0.13.3
|
| 71 |
+
tomli==2.0.1
|
| 72 |
+
torch==2.0.1
|
| 73 |
+
tqdm==4.65.0
|
| 74 |
+
transformers==4.29.2
|
| 75 |
+
typer==0.27.1
|
| 76 |
+
typing_extensions==4.6.3
|
| 77 |
+
tzdata==2023.3
|
| 78 |
+
udapi==0.5.2
|
| 79 |
+
udtools==0.2.8
|
| 80 |
+
urllib3==2.0.2
|
src/.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
src/__init__.py
ADDED
|
File without changes
|
src/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (148 Bytes). View file
|
|
|
src/__pycache__/classes.cpython-311.pyc
ADDED
|
Binary file (4.71 kB). View file
|
|
|
src/__pycache__/conll_output.cpython-311.pyc
ADDED
|
Binary file (3.38 kB). View file
|
|
|
src/__pycache__/data_preparation.cpython-311.pyc
ADDED
|
Binary file (11.4 kB). View file
|
|
|
src/__pycache__/logger.cpython-311.pyc
ADDED
|
Binary file (2.83 kB). View file
|
|
|
src/classes.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import List, Union
|
| 3 |
+
from dataclasses import astuple, dataclass
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from camel_tools.disambig.bert import BERTUnfactoredDisambiguator
|
| 6 |
+
from camel_tools.disambig.mle import MLEDisambiguator
|
| 7 |
+
from camel_tools.utils.charmap import CharMapper
|
| 8 |
+
|
| 9 |
+
def get_conll_tree_header_list():
|
| 10 |
+
return ["ID", "FORM", "LEMMA", "UPOS", "XPOS", "FEATS", "HEAD", "DEPREL", "DEPS", "MISC"]
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class ConllParams:
|
| 14 |
+
file_path: str
|
| 15 |
+
parse_model_path: str
|
| 16 |
+
|
| 17 |
+
def __iter__(self):
|
| 18 |
+
return iter(astuple(self))
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class TextParams:
|
| 22 |
+
lines: List[str]
|
| 23 |
+
parse_model_path: str
|
| 24 |
+
arclean: CharMapper
|
| 25 |
+
disambiguator_param: Union[BERTUnfactoredDisambiguator, MLEDisambiguator, str]
|
| 26 |
+
clitic_feats_df: pd.DataFrame
|
| 27 |
+
tagset: str
|
| 28 |
+
morphology_db_type: str
|
| 29 |
+
|
| 30 |
+
def __iter__(self):
|
| 31 |
+
return iter(astuple(self))
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class PreprocessedTextParams:
|
| 35 |
+
lines: List[str]
|
| 36 |
+
parse_model_path: str
|
| 37 |
+
disambiguator: Union[BERTUnfactoredDisambiguator, MLEDisambiguator, str]
|
| 38 |
+
clitic_feats_df: pd.DataFrame
|
| 39 |
+
tagset: str
|
| 40 |
+
morphology_db_type: str
|
| 41 |
+
|
| 42 |
+
def __iter__(self):
|
| 43 |
+
return iter(astuple(self))
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class TokenizedParams:
|
| 47 |
+
lines: List[str]
|
| 48 |
+
parse_model_path: str
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class TokenizedTaggedParams:
|
| 52 |
+
lines: List[str]
|
| 53 |
+
parse_model_path: str
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class Token:
|
| 57 |
+
ID: int = -1
|
| 58 |
+
FORM: str = '_'
|
| 59 |
+
LEMMA: str = '_'
|
| 60 |
+
UPOS: str = '_'
|
| 61 |
+
XPOS: str = '_'
|
| 62 |
+
FEATS: str = '_'
|
| 63 |
+
HEAD: int = 0
|
| 64 |
+
DEPREL: str = '_'
|
| 65 |
+
DEPS: str = '_'
|
| 66 |
+
MISC: str = '_'
|
| 67 |
+
|
| 68 |
+
def to_conll_row(self):
|
| 69 |
+
col_vals = [str(getattr(self, key)) for key in self.__dataclass_fields__.keys()]
|
| 70 |
+
return '\t'.join(col_vals)
|
src/conll_output.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import re
|
| 5 |
+
from typing import List, Union
|
| 6 |
+
|
| 7 |
+
from .classes import Token
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def print_to_conll(string_lines):
|
| 11 |
+
for line in string_lines:
|
| 12 |
+
print(line)
|
| 13 |
+
|
| 14 |
+
def save_to_file(string_lines: List[str], file_path: Path):
|
| 15 |
+
with open(file_path, 'w') as f:
|
| 16 |
+
[f.write(f'{line}\n') for line in string_lines]
|
| 17 |
+
|
| 18 |
+
def text_tuples_to_string(
|
| 19 |
+
text_tuples: List[List[tuple]],
|
| 20 |
+
file_type,
|
| 21 |
+
annotations: Union[List[str], None]=None,
|
| 22 |
+
sentences: Union[List[str], None]=None
|
| 23 |
+
):
|
| 24 |
+
if sentences is not None and file_type != 'conll':
|
| 25 |
+
# filter out empty lines
|
| 26 |
+
sentences = list(filter(lambda x : len(re.sub(r"\s+", "", x, flags=re.UNICODE)) > 0, sentences))
|
| 27 |
+
# get treeTokens
|
| 28 |
+
tokens = [[tup[1] for tup in sent] for sent in text_tuples]
|
| 29 |
+
|
| 30 |
+
string_lines: List[str] = []
|
| 31 |
+
for i, sentence_tuples in enumerate(text_tuples):
|
| 32 |
+
if file_type == 'conll': # dont add comments to preexisting conll files
|
| 33 |
+
pass
|
| 34 |
+
elif sentences:
|
| 35 |
+
string_lines.extend(
|
| 36 |
+
(
|
| 37 |
+
f"# text = {sentences[i].strip()}",
|
| 38 |
+
f"# treeTokens = {' '.join(tokens[i])}",
|
| 39 |
+
)
|
| 40 |
+
)
|
| 41 |
+
elif annotations:
|
| 42 |
+
string_lines.append(annotations[i])
|
| 43 |
+
|
| 44 |
+
for token_tuple in sentence_tuples:
|
| 45 |
+
token = Token(*token_tuple)
|
| 46 |
+
string_lines.append(token.to_conll_row())
|
| 47 |
+
|
| 48 |
+
string_lines.append('') # add empty line between trees
|
| 49 |
+
|
| 50 |
+
return string_lines
|
src/data_preparation.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Union
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from camel_tools.disambig.common import DisambiguatedWord
|
| 7 |
+
|
| 8 |
+
from src.utils.conll_fixes import adjust_eof_newlines
|
| 9 |
+
from .classes import ConllParams, TextParams, PreprocessedTextParams, TokenizedParams, TokenizedTaggedParams
|
| 10 |
+
from .dependency_parser.biaff_parser import parse_conll, parse_text_tuples
|
| 11 |
+
from .initialize_disambiguator.disambiguator_interface import get_disambiguator
|
| 12 |
+
from .parse_disambiguation.disambiguation_analysis import to_sentence_analysis_list
|
| 13 |
+
from .parse_disambiguation.feature_extraction import to_conll_fields_list
|
| 14 |
+
from .utils.text_cleaner import clean_lines, clean_mad, split_lines_words
|
| 15 |
+
from .logger import log
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
FileTypeParams = Union[ConllParams, TextParams, PreprocessedTextParams, TokenizedParams, TokenizedTaggedParams]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def get_feats_from_text_tuples(text_tuples: List[List[tuple]]) -> List[List[str]]:
|
| 22 |
+
"""Extract the FEATS columns from the unparsed data.
|
| 23 |
+
FEATS will exist only for text and pre-processed text inputs.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
text_tuples (List[List[tuple]]): unparsed data
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
List[List[str]]: the FEATS column (or _ if it does not exist)
|
| 30 |
+
"""
|
| 31 |
+
try:
|
| 32 |
+
return [[col_items[5] for col_items in tup_list] for tup_list in text_tuples]
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(e)
|
| 35 |
+
print('Not enough elements in tuple.')
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def add_feats(text_tuples: List[List[tuple]], text_feats: List[List[str]]) -> List[List[tuple]]:
|
| 39 |
+
"""Add FEATS data to the text tuples.
|
| 40 |
+
The parent list (text_tuples) is a list of sentences.
|
| 41 |
+
Each sentence is a list of tuples.
|
| 42 |
+
Each tuple represents a token.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
text_tuples (List[List[tuple]]): list of list of tuples
|
| 46 |
+
text_feats (List[List[str]]): list of list of FEATS
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
List[List[tuple]]: text_tuples but with the FEATS column filled
|
| 50 |
+
"""
|
| 51 |
+
text_tuples_with_feats = []
|
| 52 |
+
for sentence_tuples, sentence_feats in zip(text_tuples, text_feats):
|
| 53 |
+
|
| 54 |
+
# get first 5 and last 4 items from parsed tuple using lists, and add features.
|
| 55 |
+
# Convert the list of fields to a tuple
|
| 56 |
+
merged_tuples = [
|
| 57 |
+
tuple(list(token_tuple[:5]) + [token_feats] + list(token_tuple[6:]))
|
| 58 |
+
for token_tuple, token_feats in zip(sentence_tuples, sentence_feats)
|
| 59 |
+
]
|
| 60 |
+
text_tuples_with_feats.append(merged_tuples)
|
| 61 |
+
return text_tuples_with_feats
|
| 62 |
+
|
| 63 |
+
def string_to_tuple_list(string_of_tuples: str) -> List[tuple[str, str]]:
|
| 64 |
+
"""Take a string of space-separated tuples and convert it to a tuple list.
|
| 65 |
+
Example input: '(جامعة, NOM) (نيويورك, PROP)'
|
| 66 |
+
Example output: [(جامعة, NOM), (نيويورك, PROP)]
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
string_of_tuples (str): string of tuples
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
List(tuple[str, str]): list of token-pos tuple pairs
|
| 73 |
+
"""
|
| 74 |
+
sentence_tuples = []
|
| 75 |
+
|
| 76 |
+
# split on space, and using positive lookbehind and lookahead
|
| 77 |
+
# to detect parentheses around the space
|
| 78 |
+
for tup in re.split(r'(?<=\)) (?=\()', string_of_tuples.strip()):
|
| 79 |
+
# tup = (جامعة, NOM)
|
| 80 |
+
tup_items = tup[1:-1] # removes parens
|
| 81 |
+
form = (','.join(tup_items.split(',')[:-1])).strip() # account for comma tokens
|
| 82 |
+
pos = (tup_items.split(',')[-1]).strip()
|
| 83 |
+
sentence_tuples.append((form, pos))
|
| 84 |
+
return sentence_tuples
|
| 85 |
+
|
| 86 |
+
def get_tree_tokens(tok_pos_tuples):
|
| 87 |
+
sentences = []
|
| 88 |
+
for sentence_tuples in tok_pos_tuples:
|
| 89 |
+
sentence = ' '.join([tok_pos_tuple[0] for tok_pos_tuple in sentence_tuples])
|
| 90 |
+
sentences.append(sentence)
|
| 91 |
+
return sentences
|
| 92 |
+
|
| 93 |
+
def handle_conll(file_type_params):
|
| 94 |
+
file_path, parse_model_path = file_type_params
|
| 95 |
+
# pass the path to the text file and the model path and name, and get the tuples
|
| 96 |
+
return parse_conll(file_path, parse_model=parse_model_path)
|
| 97 |
+
|
| 98 |
+
@log
|
| 99 |
+
def disambiguate_sentences(disambiguator, token_lines):
|
| 100 |
+
# moved to own function to add to logger
|
| 101 |
+
return disambiguator.disambiguate_sentences(token_lines)
|
| 102 |
+
|
| 103 |
+
def handle_text_types(file_type_params, text_type: str):
|
| 104 |
+
if text_type == 'preprocessed_text':
|
| 105 |
+
lines, _, disambiguator_param, clitic_feats_df, tagset, morphology_db_type = file_type_params
|
| 106 |
+
|
| 107 |
+
token_lines = split_lines_words(lines)
|
| 108 |
+
token_lines = clean_mad(token_lines)
|
| 109 |
+
elif text_type == 'text':
|
| 110 |
+
lines, _, arclean, disambiguator_param, clitic_feats_df, tagset, morphology_db_type = file_type_params
|
| 111 |
+
# clean lines
|
| 112 |
+
token_lines = clean_lines(lines, arclean)
|
| 113 |
+
else:
|
| 114 |
+
assert False, f'Invalid type to process: {text_type}'
|
| 115 |
+
|
| 116 |
+
token_lines = [token_line for token_line in token_lines if token_line]
|
| 117 |
+
|
| 118 |
+
# if str passed, we should create the disambiguator using disambiguator_param and morphology_db_type
|
| 119 |
+
if type(disambiguator_param) == str:
|
| 120 |
+
disambiguator = get_disambiguator(disambiguator_param, morphology_db_type)
|
| 121 |
+
else: # a disambiguator was passed
|
| 122 |
+
disambiguator = disambiguator_param
|
| 123 |
+
|
| 124 |
+
# run the disambiguator on the sentence list to get an analysis for all sentences
|
| 125 |
+
disambiguated_sentences: List[List[DisambiguatedWord]] = disambiguate_sentences(disambiguator, token_lines)
|
| 126 |
+
# get a single analysis for each word (top or tok_match, match not implemented yet)
|
| 127 |
+
# sentence_analysis_list: List[List[dict]] = to_sentence_analysis_list(disambiguated_sentences, selection, selection_criteria)
|
| 128 |
+
sentence_analysis_list: List[List[dict]] = to_sentence_analysis_list(disambiguated_sentences, token_lines)
|
| 129 |
+
|
| 130 |
+
# extract the relevant items from each analysis into conll fields
|
| 131 |
+
return to_conll_fields_list(sentence_analysis_list, clitic_feats_df, tagset)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def handle_preprocessed_text(file_type_params):
|
| 135 |
+
return handle_text_types(file_type_params, 'preprocessed_text')
|
| 136 |
+
|
| 137 |
+
def handle_text(file_type_params):
|
| 138 |
+
return handle_text_types(file_type_params, 'text')
|
| 139 |
+
|
| 140 |
+
def handle_tokenized(file_type_params):
|
| 141 |
+
lines = file_type_params.lines
|
| 142 |
+
# construct tuples before sending them to the parser
|
| 143 |
+
return [[(0, tok, '_' ,'UNK', '_', '_', '_', '_', '_', '_') for tok in line.strip().split(' ')] for line in lines]
|
| 144 |
+
|
| 145 |
+
def handle_tokenized_tagged(file_type_params):
|
| 146 |
+
lines = file_type_params.lines
|
| 147 |
+
# convert input tuple list into a tuple data structure
|
| 148 |
+
tok_pos_tuples_list = [string_to_tuple_list(line) for line in lines]
|
| 149 |
+
# since we did not start with sentences, we make sentences using the tokens (which we call tree tokens)
|
| 150 |
+
lines = get_tree_tokens(tok_pos_tuples_list)
|
| 151 |
+
# construct tuples before sending them to the parser
|
| 152 |
+
return [[(0, tup[0],'_' ,tup[1], '_', '_', '_', '_', '_', '_') for tup in tok_pos_tuples] for tok_pos_tuples in tok_pos_tuples_list]
|
| 153 |
+
|
| 154 |
+
def get_file_type_params(lines, file_type, file_path, parse_model_path,
|
| 155 |
+
arclean, disambiguator_type, clitic_feats_df, tagset, morphology_db_type):
|
| 156 |
+
if file_type == 'conll':
|
| 157 |
+
return ConllParams(file_path, parse_model_path)
|
| 158 |
+
elif file_type == 'text':
|
| 159 |
+
return TextParams(lines, parse_model_path, arclean, disambiguator_type, clitic_feats_df, tagset, morphology_db_type)
|
| 160 |
+
elif file_type == 'preprocessed_text':
|
| 161 |
+
return PreprocessedTextParams(lines, parse_model_path, disambiguator_type, clitic_feats_df, tagset, morphology_db_type)
|
| 162 |
+
elif file_type == 'tokenized':
|
| 163 |
+
return TokenizedParams(lines, parse_model_path)
|
| 164 |
+
elif file_type == 'tokenized_tagged':
|
| 165 |
+
return TokenizedTaggedParams(lines, parse_model_path)
|
| 166 |
+
|
| 167 |
+
def parse_text(file_type: str, file_type_params: FileTypeParams):
|
| 168 |
+
if file_type == 'conll':
|
| 169 |
+
# handle_conll(file_path, parse_model_path)
|
| 170 |
+
adjust_eof_newlines(file_type_params.file_path)
|
| 171 |
+
parsed_text_tuples = handle_conll(file_type_params)
|
| 172 |
+
else:
|
| 173 |
+
text_tuples: List[List[tuple]] = []
|
| 174 |
+
if file_type == 'text':
|
| 175 |
+
text_tuples = handle_text(file_type_params)
|
| 176 |
+
elif file_type == 'preprocessed_text':
|
| 177 |
+
text_tuples = handle_preprocessed_text(file_type_params)
|
| 178 |
+
elif file_type == 'tokenized':
|
| 179 |
+
text_tuples = handle_tokenized(file_type_params)
|
| 180 |
+
elif file_type == 'tokenized_tagged':
|
| 181 |
+
text_tuples = handle_tokenized_tagged(file_type_params)
|
| 182 |
+
|
| 183 |
+
# the text tuples created from the above processes is passed to the dependency parser
|
| 184 |
+
parsed_text_tuples = parse_text_tuples(text_tuples, parse_model=str(file_type_params.parse_model_path))
|
| 185 |
+
# for text/preprocessed_text, we want to extract the features to place in parsed_text_tuples
|
| 186 |
+
# TODO: check if this step can be skipped by placing features in a step above
|
| 187 |
+
text_feats: List[List[str]] = get_feats_from_text_tuples(text_tuples)
|
| 188 |
+
# place features in FEATS column
|
| 189 |
+
parsed_text_tuples = add_feats(parsed_text_tuples, text_feats)
|
| 190 |
+
|
| 191 |
+
return parsed_text_tuples
|
| 192 |
+
|
| 193 |
+
def get_tagset(parse_model):
|
| 194 |
+
if parse_model == 'catib':
|
| 195 |
+
return 'catib6'
|
| 196 |
+
elif parse_model == 'ud':
|
| 197 |
+
return 'ud'
|
| 198 |
+
else:
|
| 199 |
+
raise f'{parse_model} model does not exist!'
|
src/dependency_parser/__init__.py
ADDED
|
File without changes
|
src/dependency_parser/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (166 Bytes). View file
|
|
|
src/dependency_parser/__pycache__/biaff_parser.cpython-311.pyc
ADDED
|
Binary file (4.72 kB). View file
|
|
|
src/dependency_parser/biaff_parser.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Union, Dict
|
| 2 |
+
|
| 3 |
+
from supar import Parser
|
| 4 |
+
from supar.utils import Dataset
|
| 5 |
+
|
| 6 |
+
from camel_tools.utils.dediac import dediac_ar
|
| 7 |
+
|
| 8 |
+
from ..logger import log
|
| 9 |
+
|
| 10 |
+
"""
|
| 11 |
+
conll object from parser
|
| 12 |
+
iterate over sentences from this object
|
| 13 |
+
iterate over the columns per sentence
|
| 14 |
+
create token rows
|
| 15 |
+
create conll sentence of token row tuples
|
| 16 |
+
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def parser_conll_to_conll_tuples(parser_conll: Dataset) -> List[List[tuple]]:
|
| 20 |
+
conll_sentences = []
|
| 21 |
+
for parser_sentence in parser_conll:
|
| 22 |
+
sentence_tree_token_tuples = []
|
| 23 |
+
for i in range(len(parser_sentence.values[0])):
|
| 24 |
+
token_tuple_row = tuple(column[i] for column in parser_sentence.values)
|
| 25 |
+
sentence_tree_token_tuples.append(token_tuple_row)
|
| 26 |
+
conll_sentences.append(sentence_tree_token_tuples)
|
| 27 |
+
return conll_sentences
|
| 28 |
+
|
| 29 |
+
def filter_tatweel(form):
|
| 30 |
+
if form.replace("_", "").replace("\u0640","").replace("\u005F", "") == "":
|
| 31 |
+
return form
|
| 32 |
+
return form.replace("_", "").replace("\u0640","").replace("\u005F", "")
|
| 33 |
+
|
| 34 |
+
@log
|
| 35 |
+
def parse(conll_path_or_parsed_tuples: Union[List[List[tuple]], str], parse_model:str) -> List[List[tuple]]:
|
| 36 |
+
parser = Parser.load(parse_model)
|
| 37 |
+
return parser.predict(conll_path_or_parsed_tuples, verbose=False, tree=True, proj=True)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def parse_text_tuples(sentence_tuples: List[List[tuple]], parse_model) -> List[List[tuple]]:
|
| 41 |
+
sentence_tuples = [[val[1:4] for val in sent] for sent in sentence_tuples]
|
| 42 |
+
form_lemma_pos_tuple = [[(filter_tatweel(dediac_ar(val[0])), filter_tatweel(dediac_ar(val[1])), val[2]) for val in sent] for sent in sentence_tuples]
|
| 43 |
+
conll = parse(form_lemma_pos_tuple, parse_model=parse_model)
|
| 44 |
+
return parser_conll_to_conll_tuples(conll)
|
| 45 |
+
|
| 46 |
+
def parse_conll(conll_path: str, parse_model) -> List[List[tuple]]:
|
| 47 |
+
conll = parse(conll_path, parse_model=parse_model)
|
| 48 |
+
for i, sent in enumerate(conll):
|
| 49 |
+
conll[i].values[1] = [filter_tatweel(form) for form in sent.values[1]]
|
| 50 |
+
return parser_conll_to_conll_tuples(conll)
|
src/initialize_disambiguator/__init__.py
ADDED
|
File without changes
|
src/initialize_disambiguator/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (173 Bytes). View file
|
|
|
src/initialize_disambiguator/__pycache__/bert_disambiguator.cpython-311.pyc
ADDED
|
Binary file (583 Bytes). View file
|
|
|
src/initialize_disambiguator/__pycache__/disambiguator_interface.cpython-311.pyc
ADDED
|
Binary file (1.7 kB). View file
|
|
|
src/initialize_disambiguator/__pycache__/mle_disambiguator.cpython-311.pyc
ADDED
|
Binary file (1.96 kB). View file
|
|
|
src/initialize_disambiguator/bert_disambiguator.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
# class BertDisambiguatorAdapter:
|
| 4 |
+
# pass
|
| 5 |
+
|
| 6 |
+
from camel_tools.disambig.bert import BERTUnfactoredDisambiguator
|
| 7 |
+
|
| 8 |
+
def create_bert_disambiguator(analyzer):
|
| 9 |
+
model = BERTUnfactoredDisambiguator.pretrained("msa", top=1000, pretrained_cache=False)
|
| 10 |
+
model._analyzer = analyzer
|
| 11 |
+
return model
|
src/initialize_disambiguator/disambiguator_interface.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Union
|
| 2 |
+
from camel_tools.morphology.database import MorphologyDB
|
| 3 |
+
from camel_tools.morphology.analyzer import Analyzer
|
| 4 |
+
from camel_tools.disambig.bert import BERTUnfactoredDisambiguator
|
| 5 |
+
|
| 6 |
+
from ..logger import log
|
| 7 |
+
from .bert_disambiguator import create_bert_disambiguator
|
| 8 |
+
from .mle_disambiguator import MLEDisambiguatorAdapter
|
| 9 |
+
|
| 10 |
+
def set_up_analyzer(morphology_db: str) -> Analyzer:
|
| 11 |
+
# used to initialize an Analyzer with ADD_PROP backoff
|
| 12 |
+
# db = MorphologyDB.builtin_db('calima-msa-s31')
|
| 13 |
+
db_type = None if morphology_db == 'r13' else morphology_db
|
| 14 |
+
db = MorphologyDB.builtin_db(db_name=db_type)
|
| 15 |
+
return Analyzer(db=db, backoff='ADD_PROP', cache_size=100000)
|
| 16 |
+
|
| 17 |
+
@log
|
| 18 |
+
def get_disambiguator(model_name: str, morphology_db: str) -> Union[MLEDisambiguatorAdapter, BERTUnfactoredDisambiguator]:
|
| 19 |
+
analyzer = set_up_analyzer(morphology_db)
|
| 20 |
+
|
| 21 |
+
if model_name == 'mle':
|
| 22 |
+
model = MLEDisambiguatorAdapter(analyzer)
|
| 23 |
+
elif model_name == 'bert':
|
| 24 |
+
model = create_bert_disambiguator(analyzer)
|
| 25 |
+
else:
|
| 26 |
+
raise ValueError('Invalid model')
|
| 27 |
+
|
| 28 |
+
return model
|
src/initialize_disambiguator/mle_disambiguator.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from camel_tools.disambig.mle import MLEDisambiguator
|
| 3 |
+
from camel_tools.disambig.common import DisambiguatedWord
|
| 4 |
+
from camel_tools.morphology.analyzer import Analyzer
|
| 5 |
+
|
| 6 |
+
class MLEDisambiguatorAdapter():
|
| 7 |
+
def __init__(self, analyzer: Analyzer):
|
| 8 |
+
self.disambiguator = MLEDisambiguator(analyzer=analyzer)
|
| 9 |
+
|
| 10 |
+
# def pretrained(self, analyzer):
|
| 11 |
+
# self.disambiguator = self.disambiguator
|
| 12 |
+
|
| 13 |
+
def disambiguate(self, sentence: List[str]) -> List[DisambiguatedWord]:
|
| 14 |
+
return self.disambiguator.disambiguate(sentence)
|
| 15 |
+
|
| 16 |
+
def disambiguate_sentences(self, lines: List[List[str]]) -> List[List[DisambiguatedWord]]:
|
| 17 |
+
return [self.disambiguator.disambiguate(line) for line in lines]
|
src/logger.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import functools
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import time
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
logging.basicConfig(level = logging.DEBUG)
|
| 9 |
+
logger = logging.getLogger()
|
| 10 |
+
|
| 11 |
+
log_path = f'{os.getcwd()}/logs.txt'
|
| 12 |
+
|
| 13 |
+
with open(log_path, 'a') as f:
|
| 14 |
+
f.write(f'[{datetime.now().strftime("%d/%b/%Y %H:%M:%S")}]\n')
|
| 15 |
+
|
| 16 |
+
def map_function_to_phrase(function_name):
|
| 17 |
+
"""Replace logged function name with a proper phrase.
|
| 18 |
+
"""
|
| 19 |
+
if function_name == "parse":
|
| 20 |
+
return "parsing duration"
|
| 21 |
+
elif function_name == "get_disambiguator":
|
| 22 |
+
return "disambiguator setup duration"
|
| 23 |
+
elif function_name == "disambiguate_sentences":
|
| 24 |
+
return "disambiguating sentences"
|
| 25 |
+
elif function_name == "main":
|
| 26 |
+
return "total execution time"
|
| 27 |
+
else:
|
| 28 |
+
return function_name
|
| 29 |
+
|
| 30 |
+
def log(func):
|
| 31 |
+
@functools.wraps(func)
|
| 32 |
+
def wrapper(*args, **kwargs):
|
| 33 |
+
try:
|
| 34 |
+
|
| 35 |
+
start_time = time.time()
|
| 36 |
+
result = func(*args, **kwargs)
|
| 37 |
+
end_time = time.time()
|
| 38 |
+
|
| 39 |
+
with open(log_path, 'a') as f:
|
| 40 |
+
f.write(f'{map_function_to_phrase(func.__name__)}: {round(end_time - start_time, 2)}s\n')
|
| 41 |
+
return result
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
|
| 44 |
+
raise e
|
| 45 |
+
|
| 46 |
+
return wrapper
|
src/parse_disambiguation/__init__.py
ADDED
|
File without changes
|
src/parse_disambiguation/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (169 Bytes). View file
|
|
|
src/parse_disambiguation/__pycache__/disambiguation_analysis.cpython-311.pyc
ADDED
|
Binary file (4.99 kB). View file
|
|
|
src/parse_disambiguation/__pycache__/feature_extraction.cpython-311.pyc
ADDED
|
Binary file (14.1 kB). View file
|
|
|
src/parse_disambiguation/disambiguation_analysis.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from typing import List
|
| 3 |
+
from camel_tools.disambig.common import DisambiguatedWord
|
| 4 |
+
from camel_tools.utils.dediac import dediac_ar
|
| 5 |
+
from ..classes import Token
|
| 6 |
+
|
| 7 |
+
def get_tok_match_analysis(disambig_word, token):
|
| 8 |
+
for i, scored_analysis in enumerate(disambig_word.analyses):
|
| 9 |
+
if dediac_ar(scored_analysis.analysis['diac']) == dediac_ar(token):
|
| 10 |
+
if i != 0:
|
| 11 |
+
# print(f"#NOT_FIRST Input token: {token}, analysis: {get_first_analysis(disambig_word)['diac']}", file=sys.stderr)
|
| 12 |
+
print(f"#NOT_FIRST {token}", file=sys.stderr)
|
| 13 |
+
|
| 14 |
+
return scored_analysis.analysis
|
| 15 |
+
|
| 16 |
+
# print(f"#NOAN token {token}, analysis: {get_first_analysis(disambig_word)['diac']}", file=sys.stderr)
|
| 17 |
+
print(f"#NOAN {token}", file=sys.stderr)
|
| 18 |
+
|
| 19 |
+
return get_first_analysis(disambig_word) # no token match, so just return first
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_analysis_by_criteria(disambig_word: DisambiguatedWord, selection_criteria: dict) -> dict:
|
| 23 |
+
# runs assertions to ensure the data is good
|
| 24 |
+
is_analysis(disambig_word)
|
| 25 |
+
|
| 26 |
+
for scored_analysis in disambig_word.analyses:
|
| 27 |
+
analysis = scored_analysis.analysis
|
| 28 |
+
if all(analysis[key] == selection_criteria[key] for key in selection_criteria):
|
| 29 |
+
return analysis
|
| 30 |
+
|
| 31 |
+
# if the criteria is not found, return the first analysis
|
| 32 |
+
print(f"No analysis found for the given criteria on {disambig_word.word}, will return the first analysis.")
|
| 33 |
+
return get_first_analysis(disambig_word)
|
| 34 |
+
|
| 35 |
+
def is_analysis(disambig_word: DisambiguatedWord) -> dict:
|
| 36 |
+
# this should not be empty
|
| 37 |
+
assert disambig_word, "Disambiguated word not found!"
|
| 38 |
+
|
| 39 |
+
# this could be empty if there is no backoff, and this code assumes backoff
|
| 40 |
+
assert disambig_word.analyses, "No analyses found, are you using backoff?"
|
| 41 |
+
assert disambig_word.analyses[0], "No analyses found, are you using backoff?"
|
| 42 |
+
|
| 43 |
+
def get_first_analysis(disambig_word: DisambiguatedWord):
|
| 44 |
+
# runs assertions to ensure the data is good
|
| 45 |
+
is_analysis(disambig_word)
|
| 46 |
+
|
| 47 |
+
return disambig_word.analyses[0].analysis
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def get_sentence_analysis(disambiguated_sentence: List[DisambiguatedWord], token_line, selection, selection_criteria) -> List[dict]:
|
| 51 |
+
# sourcery skip: switch
|
| 52 |
+
# get an analysis based on the selection criteria
|
| 53 |
+
# top means take first
|
| 54 |
+
# tok_match takes the first analysis that matches diac, otherwise takes first
|
| 55 |
+
# match means take the first analysis that matches the selection criteria.
|
| 56 |
+
# selection criteria: {'criteria1': 'value1', 'criteria2': 'value2'...}
|
| 57 |
+
# return None if criteria not fulfilled.
|
| 58 |
+
# TODO: check if selection criteria contains valid keys
|
| 59 |
+
if selection == 'top':
|
| 60 |
+
return [get_first_analysis(disambig_word) for disambig_word in disambiguated_sentence]
|
| 61 |
+
elif selection == 'tok_match':
|
| 62 |
+
return [get_tok_match_analysis(disambig_word, token) for disambig_word, token in zip(disambiguated_sentence, token_line)]
|
| 63 |
+
elif selection == 'match':
|
| 64 |
+
return [get_analysis_by_criteria(disambig_word, selection_criteria) for disambig_word in disambiguated_sentence]
|
| 65 |
+
else:
|
| 66 |
+
raise ValueError(f"the selection {selection} is not valid!")
|
| 67 |
+
|
| 68 |
+
def to_sentence_analysis_list(disambiguated_sentences: List[List[DisambiguatedWord]], token_lines, selection: str='tok_match', selection_criteria: dict=None) -> List[List[Token]]:
|
| 69 |
+
return [get_sentence_analysis(disambiguated_sentence, token_line, selection, selection_criteria) for disambiguated_sentence, token_line in zip(disambiguated_sentences, token_lines)]
|
src/parse_disambiguation/feature_extraction.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Given a list of list of analyses (one analysis per token),
|
| 2 |
+
get features based on a given criteria.
|
| 3 |
+
|
| 4 |
+
If no criteria is given, return atbtok and catib6
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import re
|
| 8 |
+
from typing import List
|
| 9 |
+
import json
|
| 10 |
+
from camel_tools.utils.dediac import dediac_ar
|
| 11 |
+
from camel_tools.utils.charmap import CharMapper
|
| 12 |
+
from camel_tools.utils.transliterate import Transliterator
|
| 13 |
+
import pandas as pd
|
| 14 |
+
|
| 15 |
+
FEATURES_LIST = ["pos", "prc3", "prc2", "prc1", "prc0", "enc0", "asp", "vox", "mod", "gen", "num", "stt", "cas", "per", "rat"]
|
| 16 |
+
|
| 17 |
+
def feats_dict_to_string(feats_dict):
|
| 18 |
+
# prc3=na|prc2=na|prc1=na|prc0=na|per=na|asp=na|vox=na|mod=na|gen=na|num=na|stt=na|cas=na|enc0=na|rat=na
|
| 19 |
+
feats_str = json.dumps(feats_dict)
|
| 20 |
+
return feats_str.replace('", "', "|").replace('": "', "=").replace('{"', '').replace('"}', '')
|
| 21 |
+
|
| 22 |
+
def build_clitic_feats_dict(clitic_feat_list):
|
| 23 |
+
assert len(clitic_feat_list) != 0, f'invalid clitic, {clitic_feat_list}'
|
| 24 |
+
assert len(clitic_feat_list) == 1, f'multiple clitics matched when only one should, {clitic_feat_list}'
|
| 25 |
+
clitic_feat_list[0]['deciding_feat']
|
| 26 |
+
final_clitic_feats = {k: v for k,v in clitic_feat_list[0].items() if k in FEATURES_LIST}
|
| 27 |
+
final_clitic_feats['token_type'] = clitic_feat_list[0]['deciding_feat'].split(':')[0]
|
| 28 |
+
return final_clitic_feats
|
| 29 |
+
|
| 30 |
+
def get_clitic_feats(token, clitic_order, clitic_feats, stem_feats):
|
| 31 |
+
mapper = CharMapper.builtin_mapper('ar2bw')
|
| 32 |
+
transliterator = Transliterator(mapper)
|
| 33 |
+
token = transliterator.transliterate(dediac_ar(token))
|
| 34 |
+
filtered_clitics = clitic_feats[(clitic_feats.clitic == token) & (clitic_feats.deciding_feat.str.startswith(clitic_order))]
|
| 35 |
+
clitic_list = [f'{k}:{v}' for k, v in stem_feats.items() if k.startswith(clitic_order) and v not in ['0', 'na']]
|
| 36 |
+
|
| 37 |
+
for feat_check in clitic_list:
|
| 38 |
+
clitic_feat_list = filtered_clitics[filtered_clitics.deciding_feat == feat_check].to_dict('records')
|
| 39 |
+
if clitic_feat_list:
|
| 40 |
+
return build_clitic_feats_dict(clitic_feat_list)
|
| 41 |
+
assert False, f"clitic '{token}' does not exist in clitics list. Stem features: {stem_feats}"
|
| 42 |
+
|
| 43 |
+
def get_stem_feats(word_analysis):
|
| 44 |
+
return {feat: word_analysis[feat] for feat in FEATURES_LIST}
|
| 45 |
+
|
| 46 |
+
def get_clitic_order(token):
|
| 47 |
+
if token.endswith('+'):
|
| 48 |
+
clitic_order = 'prc'
|
| 49 |
+
elif token.startswith('+'):
|
| 50 |
+
clitic_order = 'enc'
|
| 51 |
+
return clitic_order
|
| 52 |
+
|
| 53 |
+
def is_clitic(token):
|
| 54 |
+
return (token.startswith('+') or token.endswith('+')) and not re.match(r'^\++$', token)
|
| 55 |
+
|
| 56 |
+
def empty_clitic_feats_from_baseword(stem_feats):
|
| 57 |
+
clitic_type_list = ["prc3", "prc2", "prc1", "prc0", "enc0"]
|
| 58 |
+
for clitic_type in clitic_type_list:
|
| 59 |
+
if stem_feats[clitic_type] not in ['0', 'na', 'Al_det']:
|
| 60 |
+
stem_feats[clitic_type] = '0'
|
| 61 |
+
return stem_feats
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def r13_fixes(token, stem_feats):
|
| 65 |
+
# handling edge cases where li comes from variations of wa li>n (i.e. li>nhu, li>nanY)
|
| 66 |
+
li_feats = {'pos': 'conj_sub', 'prc3': '0', 'prc2': 'wa_conj', 'prc1': '0', 'prc0': 'na', 'enc0': '3ms_pron', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'na'}
|
| 67 |
+
if {k for k, _ in stem_feats.items() ^ li_feats.items()} == {'enc0'}:
|
| 68 |
+
stem_feats['prc1'] = 'li_conj'
|
| 69 |
+
|
| 70 |
+
# handling edge cases where li comes from variations of li>n (i.e. li>nhu, li>nanY)
|
| 71 |
+
li_feats = {'pos': 'conj_sub', 'prc3': '0', 'prc2': '0', 'prc1': '0', 'prc0': 'na', 'enc0': '0', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'na'}
|
| 72 |
+
if {k for k, _ in stem_feats.items() ^ li_feats.items()} == {'enc0'}:
|
| 73 |
+
stem_feats['prc1'] = 'li_conj'
|
| 74 |
+
|
| 75 |
+
# handling edge cases where li comes from li>n
|
| 76 |
+
if token == 'لِ+' and stem_feats == {'pos': 'conj_sub', 'prc3': '0', 'prc2': '0', 'prc1': '0', 'prc0': 'na', 'enc0': '0', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'na'}:
|
| 77 |
+
stem_feats['prc1'] = 'li_conj'
|
| 78 |
+
|
| 79 |
+
# handling edge cases where li comes from wa li>n
|
| 80 |
+
if token == 'لِ+' and stem_feats == {'pos': 'conj_sub', 'prc3': '0', 'prc2': 'wa_conj', 'prc1': '0', 'prc0': 'na', 'enc0': '3ms_pron', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'na'}:
|
| 81 |
+
stem_feats['prc1'] = 'li_conj'
|
| 82 |
+
|
| 83 |
+
# handling edge cases where mA comes from qlmA
|
| 84 |
+
if token == '+ما' and stem_feats == {'pos': 'conj', 'prc3': '0', 'prc2': '0', 'prc1': '0', 'prc0': 'na', 'enc0': '0', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'n'}:
|
| 85 |
+
stem_feats['enc0'] = 'mA_sub'
|
| 86 |
+
|
| 87 |
+
# handling an edge case where li comes from likY
|
| 88 |
+
# added li_conj to clitic_feats.csv
|
| 89 |
+
if token == 'لِ+' and stem_feats == {'pos': 'conj', 'prc3': '0', 'prc2': '0', 'prc1': '0', 'prc0': 'na', 'enc0': '0', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'na'}:
|
| 90 |
+
stem_feats['prc1'] = 'li_conj'
|
| 91 |
+
|
| 92 |
+
def add_remaining_features(tokens_df, stem_feats, clitic_feats):
|
| 93 |
+
existing_clitics = ['prc0']
|
| 94 |
+
|
| 95 |
+
clitic_feats_list = []
|
| 96 |
+
|
| 97 |
+
for _, row in tokens_df.iterrows():
|
| 98 |
+
token = row['token']
|
| 99 |
+
if not is_clitic(token):
|
| 100 |
+
baseword_feats_dict = empty_clitic_feats_from_baseword(dict(stem_feats))
|
| 101 |
+
baseword_feats_dict['token_type'] = 'baseword'
|
| 102 |
+
clitic_feats_list.append(baseword_feats_dict)
|
| 103 |
+
else:
|
| 104 |
+
clitic_order = get_clitic_order(token)
|
| 105 |
+
# handling an edge case where lA is negative
|
| 106 |
+
if token == 'لِ+' and \
|
| 107 |
+
(stem_feats == {'pos': 'conj_sub', 'prc3': '0', 'prc2': '0', 'prc1': '0', 'prc0': 'na', 'enc0': 'lA_neg', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'na'}
|
| 108 |
+
or stem_feats == {'pos': 'conj_sub', 'prc3': '0', 'prc2': 'fa_conj', 'prc1': '0', 'prc0': 'na', 'enc0': 'lA_neg', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'na'}
|
| 109 |
+
or stem_feats == {'pos': 'conj_sub', 'prc3': '0', 'prc2': 'wa_part', 'prc1': '0', 'prc0': 'na', 'enc0': 'lA_neg', 'asp': 'na', 'vox': 'na', 'mod': 'na', 'gen': 'na', 'num': 'na', 'stt': 'na', 'cas': 'na', 'per': 'na', 'rat': 'na'}
|
| 110 |
+
):
|
| 111 |
+
stem_feats['prc1'] = 'li_prep'
|
| 112 |
+
|
| 113 |
+
r13_fixes(token, stem_feats)
|
| 114 |
+
|
| 115 |
+
clitic_feats_list.append(get_clitic_feats(token.replace('+', ''), clitic_order, clitic_feats, stem_feats))
|
| 116 |
+
|
| 117 |
+
feats_df = pd.DataFrame(clitic_feats_list)
|
| 118 |
+
assert tokens_df.shape[0] == feats_df.shape[0], f'token-feature mismatch!,\ntokens: \n{tokens_df},\n\n features: \n{feats_df}'
|
| 119 |
+
|
| 120 |
+
return pd.concat([tokens_df, feats_df], axis=1)
|
| 121 |
+
|
| 122 |
+
def get_lemmas(lemma, tokens):
|
| 123 |
+
lemmas = []
|
| 124 |
+
for token in tokens:
|
| 125 |
+
if '+' in token:
|
| 126 |
+
lemmas.append(token)
|
| 127 |
+
else:
|
| 128 |
+
lemmas.append(lemma)
|
| 129 |
+
return lemmas
|
| 130 |
+
|
| 131 |
+
def get_main_features_df(word_analysis):
|
| 132 |
+
# if there are no clitics
|
| 133 |
+
if '+' not in word_analysis['catib6']:
|
| 134 |
+
tokens = [word_analysis['atbtok']]
|
| 135 |
+
catib6 = [word_analysis['catib6']]
|
| 136 |
+
ud = [word_analysis['ud']]
|
| 137 |
+
lemmas = [word_analysis['lex']]
|
| 138 |
+
else:
|
| 139 |
+
tokens = word_analysis['atbtok'].split('_')
|
| 140 |
+
catib6 = word_analysis['catib6'].split('+')
|
| 141 |
+
ud = word_analysis['ud'].split('+')
|
| 142 |
+
lemmas = get_lemmas(word_analysis['lex'], tokens)
|
| 143 |
+
|
| 144 |
+
if len(catib6) < len(tokens):
|
| 145 |
+
print(tokens)
|
| 146 |
+
print(catib6)
|
| 147 |
+
catib6.append("NOM")
|
| 148 |
+
ud.append("NOUN")
|
| 149 |
+
return pd.DataFrame({'token': tokens, 'catib6': catib6, 'ud': ud, 'lemma': lemmas})
|
| 150 |
+
elif len(tokens) < len(catib6):
|
| 151 |
+
return pd.DataFrame({'token': tokens, 'catib6': catib6[:len(tokens)], 'ud': ud[:len(tokens)], 'lemma': lemmas})
|
| 152 |
+
|
| 153 |
+
return pd.DataFrame({'token': tokens, 'catib6': catib6, 'ud': ud, 'lemma': lemmas})
|
| 154 |
+
|
| 155 |
+
def get_word_features_df(word_analysis, clitic_feats):
|
| 156 |
+
"""if a word is composed of multiple tokens, return them all.
|
| 157 |
+
otherwise, just return the word and catib6 tag
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
word_analysis (dict): analysis generated from a cameltools disambiguator
|
| 161 |
+
|
| 162 |
+
Returns:
|
| 163 |
+
List[tuple]: a list of one or more tokens
|
| 164 |
+
"""
|
| 165 |
+
# gets forms, tokens, feats, ...
|
| 166 |
+
main_feats = get_main_features_df(word_analysis)
|
| 167 |
+
stem_feats = get_stem_feats(word_analysis)
|
| 168 |
+
return add_remaining_features(main_feats, stem_feats, clitic_feats)
|
| 169 |
+
|
| 170 |
+
def join_feats(word_feats_df, tagset):
|
| 171 |
+
word_features = {
|
| 172 |
+
'tokens': list(word_feats_df['token']),
|
| 173 |
+
'pos_tags': list(word_feats_df[tagset]),
|
| 174 |
+
'lemmas': list(word_feats_df['lemma']),
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
word_feats_df.drop(['token', tagset, 'lemma'], axis=1, inplace=True)
|
| 178 |
+
feats = word_feats_df.to_dict('records')
|
| 179 |
+
word_features['feats'] = [feats_dict_to_string(row) for row in feats]
|
| 180 |
+
|
| 181 |
+
return word_features
|
| 182 |
+
|
| 183 |
+
def update_sentence_features(sentence_features, word_features):
|
| 184 |
+
sentence_features['tokens'] += word_features['tokens']
|
| 185 |
+
sentence_features['lemmas'] += word_features['lemmas']
|
| 186 |
+
sentence_features['pos_tags'] += word_features['pos_tags']
|
| 187 |
+
sentence_features['feats'] += word_features['feats']
|
| 188 |
+
|
| 189 |
+
return sentence_features
|
| 190 |
+
|
| 191 |
+
def build_token_list(sentence_features):
|
| 192 |
+
return [
|
| 193 |
+
(idx, dediac_ar(token), lemma, pos_tag, '_', feats, '_', '_', '_', '_')
|
| 194 |
+
for idx, (token, lemma, pos_tag, feats) in enumerate(
|
| 195 |
+
zip(sentence_features['tokens'], sentence_features['lemmas'], sentence_features['pos_tags'], sentence_features['feats'])
|
| 196 |
+
, 1)
|
| 197 |
+
]
|
| 198 |
+
|
| 199 |
+
def to_conll_fields_list(sentence_analysis_list: List[List[dict]], clitic_feats, tagset):
|
| 200 |
+
sentence_features_list = []
|
| 201 |
+
|
| 202 |
+
for sentence_analysis in sentence_analysis_list:
|
| 203 |
+
sentence_features = {'tokens': [], 'lemmas': [], 'pos_tags': [], 'feats': []}
|
| 204 |
+
for word_analysis in sentence_analysis:
|
| 205 |
+
word_features_df = get_word_features_df(word_analysis, clitic_feats)
|
| 206 |
+
|
| 207 |
+
word_features = join_feats(word_features_df, tagset)
|
| 208 |
+
sentence_features = update_sentence_features(sentence_features, word_features)
|
| 209 |
+
token_list = build_token_list(sentence_features)
|
| 210 |
+
sentence_features_list.append(token_list)
|
| 211 |
+
|
| 212 |
+
return sentence_features_list
|
src/utils/__init__.py
ADDED
|
File without changes
|
src/utils/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (154 Bytes). View file
|
|
|
src/utils/__pycache__/conll_fixes.cpython-311.pyc
ADDED
|
Binary file (2.29 kB). View file
|
|
|
src/utils/__pycache__/model_downloader.cpython-311.pyc
ADDED
|
Binary file (2.15 kB). View file
|
|
|
src/utils/__pycache__/text_cleaner.cpython-311.pyc
ADDED
|
Binary file (2.56 kB). View file
|
|
|
src/utils/conll_fixes.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The parsing code requires that conll files have two newlines at the end.
|
| 2 |
+
The functions below ensure that this is the case before parsing"""
|
| 3 |
+
|
| 4 |
+
def read_n_to_last_line(filename, n = 1):
|
| 5 |
+
"""Returns the nth before last line of a file (n=1 gives last line)
|
| 6 |
+
|
| 7 |
+
from https://stackoverflow.com/questions/46258499/how-to-read-the-last-line-of-a-file-in-python"""
|
| 8 |
+
num_newlines = 0
|
| 9 |
+
with open(filename, 'rb') as f:
|
| 10 |
+
try:
|
| 11 |
+
for i in range(1, n+1):
|
| 12 |
+
f.seek(-i, 2)
|
| 13 |
+
# while num_newlines < n:
|
| 14 |
+
# f.seek(-2, os.SEEK_CUR)
|
| 15 |
+
# if f.read(1) == b'\n':
|
| 16 |
+
# num_newlines += 1
|
| 17 |
+
except OSError:
|
| 18 |
+
f.seek(0)
|
| 19 |
+
last_line = f.readline().decode()
|
| 20 |
+
return last_line
|
| 21 |
+
|
| 22 |
+
def add_newlines(file_path, second_last_line, last_line):
|
| 23 |
+
with open(file_path, 'a') as f:
|
| 24 |
+
if second_last_line != '\n':
|
| 25 |
+
f.write('\n')
|
| 26 |
+
if last_line != '\n':
|
| 27 |
+
f.write('\n')
|
| 28 |
+
|
| 29 |
+
def adjust_eof_newlines(file_path):
|
| 30 |
+
second_last_line = read_n_to_last_line(file_path, 2)
|
| 31 |
+
last_line = read_n_to_last_line(file_path, 1)
|
| 32 |
+
if not (second_last_line == last_line == '\n'):
|
| 33 |
+
add_newlines(file_path, second_last_line, last_line)
|
src/utils/model_downloader.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" Script used to set up different parts of the text_to_conll_cli.py"""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from huggingface_hub import hf_hub_download
|
| 6 |
+
|
| 7 |
+
def download_default_models(model_path: Path) -> None:
|
| 8 |
+
# check if models folder exists
|
| 9 |
+
if not os.path.exists(model_path):
|
| 10 |
+
os.mkdir(model_path)
|
| 11 |
+
|
| 12 |
+
# check if default models exist
|
| 13 |
+
if not os.path.exists(model_path / "CAMeLBERT-CATiB-biaffine.model"):
|
| 14 |
+
print('downloading catib model')
|
| 15 |
+
hf_hub_download(repo_id="CAMeL-Lab/camelbert-catib-parser", filename="CAMeLBERT-CATiB-biaffine.model", local_dir=model_path)
|
| 16 |
+
if not os.path.exists(model_path / "CAMeLBERT-UD-biaffine.model"):
|
| 17 |
+
print('downloading ud model')
|
| 18 |
+
hf_hub_download(repo_id="CAMeL-Lab/camelbert-ud-parser", filename="CAMeLBERT-UD-biaffine.model", local_dir=model_path)
|
| 19 |
+
print('Default models downloaded.')
|
| 20 |
+
|
| 21 |
+
def get_model_name(parse_model: str, model_path: Path) -> str:
|
| 22 |
+
if parse_model == "catib":
|
| 23 |
+
if os.path.exists(model_path / "CAMeLBERT-CATiB-biaffine.model"):
|
| 24 |
+
return "CAMeLBERT-CATiB-biaffine.model"
|
| 25 |
+
else:
|
| 26 |
+
assert False, "CATiB default model not found. Did you run 'python download_models'?"
|
| 27 |
+
elif parse_model == "ud":
|
| 28 |
+
if os.path.exists(model_path / "CAMeLBERT-UD-biaffine.model"):
|
| 29 |
+
return "CAMeLBERT-UD-biaffine.model"
|
| 30 |
+
else:
|
| 31 |
+
assert False, "UD default model not found. Did you run 'python download_models'?"
|
| 32 |
+
else:
|
| 33 |
+
return parse_model
|
src/utils/text_cleaner.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from camel_tools.tokenizers.word import simple_word_tokenize
|
| 2 |
+
from camel_tools.utils.dediac import dediac_ar
|
| 3 |
+
from camel_tools.utils.normalize import normalize_unicode
|
| 4 |
+
from camel_tools.utils.charmap import CharMapper
|
| 5 |
+
|
| 6 |
+
def clean_line(line, arclean):
|
| 7 |
+
return simple_word_tokenize(arclean(dediac_ar(normalize_unicode(line.strip()))))
|
| 8 |
+
|
| 9 |
+
def split_lines_words(lines):
|
| 10 |
+
return [line.strip().split() for line in lines]
|
| 11 |
+
|
| 12 |
+
def clean_mad(lines):
|
| 13 |
+
new_lines = []
|
| 14 |
+
for line in lines:
|
| 15 |
+
new_line = []
|
| 16 |
+
for word in line:
|
| 17 |
+
# replacing underscore with mad character
|
| 18 |
+
if word == 'ـ':
|
| 19 |
+
new_line.append('_')
|
| 20 |
+
else:
|
| 21 |
+
new_line.append(word)
|
| 22 |
+
new_lines.append(new_line)
|
| 23 |
+
return new_lines
|
| 24 |
+
|
| 25 |
+
def clean_lines(lines, arclean):
|
| 26 |
+
return [clean_line(line, arclean) for line in lines]
|
| 27 |
+
|
| 28 |
+
if __name__ == '__main__':
|
| 29 |
+
lines = []
|
| 30 |
+
with open('data/sample_text.txt', 'r') as f:
|
| 31 |
+
lines = f.readlines()
|
| 32 |
+
|
| 33 |
+
arclean = CharMapper.builtin_mapper("arclean")
|
| 34 |
+
new_lines = clean_lines(lines, arclean)
|
tests/test_feats.tsv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
token catib6 ud lemma prc3 prc2 prc1 prc0 per asp vox mod gen num stt cas enc0 rat
|
| 2 |
+
بِ+ PRT ADP بِ+ 0 0 0 na na na na na na na na na 0 na
|
| 3 |
+
اِسْمِ NOM NOUN ٱِسْم 0 0 bi_prep 0 na na na na m s c g 0 i
|
tests/test_feature_extraction.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from pandas import read_csv
|
| 3 |
+
|
| 4 |
+
from src.parse_disambiguation.feature_extraction import get_word_features_df, join_feats
|
| 5 |
+
|
| 6 |
+
@pytest.fixture
|
| 7 |
+
def word_analysis():
|
| 8 |
+
return {'diac': 'بِسْمِ', 'lex': 'ٱِسْم', 'caphi': 'b_i_s_m_i',
|
| 9 |
+
'gloss': 'in/by_+_(the)_Name_of_+_[def.gen.]',
|
| 10 |
+
'bw': 'بِ/PREP+ٱِسْم/NOUN+ِ/CASE_DEF_GEN', 'pos': 'noun',
|
| 11 |
+
'catib6': 'PRT+NOM', 'ud': 'ADP+NOUN', 'root': 'س.م.#',
|
| 12 |
+
'pattern': 'بِ1ْ2ِ', 'prc3': '0', 'prc2': '0', 'prc1':
|
| 13 |
+
'bi_prep', 'prc0': '0', 'per': 'na', 'asp': 'na',
|
| 14 |
+
'vox': 'na', 'mod': 'na', 'form_gen': 'm', 'gen': 'm',
|
| 15 |
+
'form_num': 's', 'num': 's', 'stt': 'c', 'cas': 'g',
|
| 16 |
+
'enc0': '0', 'rat': 'i', 'source': 'lex', 'd1seg': 'بِسْمِ',
|
| 17 |
+
'd2seg': 'بِ+_سْمِ', 'd3seg': 'بِ+_سْمِ', 'atbseg': 'بِ+_سْمِ',
|
| 18 |
+
'd1tok': 'بِسْمِ', 'd2tok': 'بِ+_اِسْمِ', 'd3tok': 'بِ+_اِسْمِ',
|
| 19 |
+
'atbtok': 'بِ+_اِسْمِ', 'bwtok': 'بِ+_ٱِسْم_+ِ', 'pos_logprob': -0.4344233,
|
| 20 |
+
'lex_logprob': -3.156274, 'pos_lex_logprob': -3.156274,
|
| 21 |
+
'stem': 'بِسْمِ', 'stemgloss': 'in/by_+_(the)_Name_of_+_[def.gen.]', 'stemcat': 'FW-Wa'
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
@pytest.fixture
|
| 25 |
+
def clitic_feats():
|
| 26 |
+
return read_csv('data/clitic_feats.csv')
|
| 27 |
+
|
| 28 |
+
@pytest.fixture
|
| 29 |
+
def word_feats():
|
| 30 |
+
df = read_csv('tests/test_feats.tsv', sep='\t')
|
| 31 |
+
return df.astype(str).astype(object)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_get_word_features_df(word_analysis, clitic_feats, word_feats):
|
| 35 |
+
tagset = "catib6"
|
| 36 |
+
|
| 37 |
+
df = get_word_features_df(word_analysis, clitic_feats)
|
| 38 |
+
df = df.astype(str).astype(object)
|
| 39 |
+
assert word_feats.equals(df)
|
| 40 |
+
|
| 41 |
+
def test_join_feats_catib6(word_feats):
|
| 42 |
+
word_feats = join_feats(word_feats, 'catib6')
|
| 43 |
+
|
| 44 |
+
assert word_feats['tokens'] == ['بِ+', 'اِسْمِ']
|
| 45 |
+
assert word_feats['pos_tags'] == ['PRT', 'NOM']
|
| 46 |
+
assert word_feats['lemmas'] == ['بِ+', 'ٱِسْم']
|
| 47 |
+
assert word_feats['feats'] == ['ud=ADP|prc3=0|prc2=0|prc1=0|prc0=na|per=na|asp=na|vox=na|mod=na|gen=na|num=na|stt=na|cas=na|enc0=0|rat=na', 'ud=NOUN|prc3=0|prc2=0|prc1=bi_prep|prc0=0|per=na|asp=na|vox=na|mod=na|gen=m|num=s|stt=c|cas=g|enc0=0|rat=i']
|
| 48 |
+
|
| 49 |
+
def test_join_feats_ud(word_feats):
|
| 50 |
+
word_feats = join_feats(word_feats, 'ud')
|
| 51 |
+
|
| 52 |
+
assert word_feats['tokens'] == ['بِ+', 'اِسْمِ']
|
| 53 |
+
assert word_feats['pos_tags'] == ['ADP', 'NOUN']
|
| 54 |
+
assert word_feats['lemmas'] == ['بِ+', 'ٱِسْم']
|
| 55 |
+
assert word_feats['feats'] == ['catib6=PRT|prc3=0|prc2=0|prc1=0|prc0=na|per=na|asp=na|vox=na|mod=na|gen=na|num=na|stt=na|cas=na|enc0=0|rat=na', 'catib6=NOM|prc3=0|prc2=0|prc1=bi_prep|prc0=0|per=na|asp=na|vox=na|mod=na|gen=m|num=s|stt=c|cas=g|enc0=0|rat=i']
|
text_to_conll_cli.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Disambiguator and Conll builder CLI.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
text_to_conll_cli (-i <input> | --input=<input> | -s <string> | --string=<string>)
|
| 6 |
+
(-f <file_type> | --file_type=<file_type>)
|
| 7 |
+
[-b <morphology_db_type> | --morphology_db_type=<morphology_db_type>]
|
| 8 |
+
[-d <disambiguator> | --disambiguator=<disambiguator>]
|
| 9 |
+
[-m <model> | --model=<model>]
|
| 10 |
+
text_to_conll_cli (-h | --help)
|
| 11 |
+
|
| 12 |
+
Options:
|
| 13 |
+
-i <input> --input=<input>
|
| 14 |
+
A text file or conll file.
|
| 15 |
+
-s <string> --string=<string>
|
| 16 |
+
A string to parse.
|
| 17 |
+
-f <file_type> --file_type=<file_type>
|
| 18 |
+
The type of file passed. Could be
|
| 19 |
+
conll: conll
|
| 20 |
+
text: raw text
|
| 21 |
+
preprocessed_text: whitespace tokenized text (text will not be cleaned)
|
| 22 |
+
tokenized_tagged: text is already tokenized and POS tagged, in tuple form
|
| 23 |
+
tokenized: text is already tokenized, only parse tokenized input; don't disambiguate to add POS tags or features
|
| 24 |
+
-b <morphology_db_type> --morphology_db_type=<morphology_db_type>
|
| 25 |
+
The morphology database to use; will use camel_tools built-in by default [default: r13]
|
| 26 |
+
-d <disambiguator> --disambiguator=<disambiguator>
|
| 27 |
+
The disambiguation technique used to tokenize the text lines, either 'mle' or 'bert' [default: bert]
|
| 28 |
+
-m <model> --model=<model>
|
| 29 |
+
The name BERT model used to parse (to be placed in the model directory) [default: catib]
|
| 30 |
+
-h --help
|
| 31 |
+
Show this screen.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
from src.logger import log
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
from camel_tools.utils.charmap import CharMapper
|
| 37 |
+
from src.conll_output import print_to_conll, text_tuples_to_string
|
| 38 |
+
from src.data_preparation import get_file_type_params, get_tagset, parse_text
|
| 39 |
+
from src.utils.model_downloader import get_model_name
|
| 40 |
+
from docopt import docopt
|
| 41 |
+
from transformers.utils import logging
|
| 42 |
+
from pandas import read_csv
|
| 43 |
+
|
| 44 |
+
arguments = docopt(__doc__)
|
| 45 |
+
|
| 46 |
+
logging.set_verbosity_error()
|
| 47 |
+
|
| 48 |
+
def get_file_type(file_type):
|
| 49 |
+
if file_type in ['conll', 'text', 'preprocessed_text', 'tokenized_tagged', 'tokenized']:
|
| 50 |
+
return file_type
|
| 51 |
+
assert False, 'Unknown file type'
|
| 52 |
+
|
| 53 |
+
@log
|
| 54 |
+
def main():
|
| 55 |
+
root_dir = Path(__file__).parent
|
| 56 |
+
model_path = root_dir/"models"
|
| 57 |
+
|
| 58 |
+
# camel_tools import used to clean text
|
| 59 |
+
arclean = CharMapper.builtin_mapper("arclean")
|
| 60 |
+
|
| 61 |
+
#
|
| 62 |
+
### Get clitic features
|
| 63 |
+
#
|
| 64 |
+
clitic_feats_df = read_csv(root_dir / 'data/clitic_feats.csv')
|
| 65 |
+
clitic_feats_df = clitic_feats_df.astype(str).astype(object) # so ints read are treated as string objects
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
#
|
| 69 |
+
### cli user input ###
|
| 70 |
+
#
|
| 71 |
+
file_path = arguments['--input']
|
| 72 |
+
string_text = arguments['--string']
|
| 73 |
+
file_type = get_file_type(arguments['--file_type'])
|
| 74 |
+
morphology_db_type = arguments['--morphology_db_type']
|
| 75 |
+
disambiguator_type = arguments['--disambiguator']
|
| 76 |
+
parse_model = arguments['--model']
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
#
|
| 80 |
+
### Set up parsing model
|
| 81 |
+
# (download defaults models, and get correct model name from the models directory)
|
| 82 |
+
#
|
| 83 |
+
model_name = get_model_name(parse_model, model_path=model_path)
|
| 84 |
+
|
| 85 |
+
#
|
| 86 |
+
### get tagset (depends on model)
|
| 87 |
+
#
|
| 88 |
+
tagset = get_tagset(parse_model)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
#
|
| 92 |
+
### main code ###
|
| 93 |
+
#
|
| 94 |
+
lines = []
|
| 95 |
+
if string_text is not None:
|
| 96 |
+
lines = [string_text]
|
| 97 |
+
elif file_path is not None:
|
| 98 |
+
with open(file_path, 'r') as f:
|
| 99 |
+
lines = [line for line in f.readlines() if line.strip()]
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
file_type_params = get_file_type_params(lines, file_type, file_path, model_path/model_name,
|
| 103 |
+
arclean, disambiguator_type, clitic_feats_df, tagset, morphology_db_type)
|
| 104 |
+
parsed_text_tuples = parse_text(file_type, file_type_params)
|
| 105 |
+
|
| 106 |
+
string_lines = text_tuples_to_string(parsed_text_tuples, file_type, sentences=lines)
|
| 107 |
+
print_to_conll(string_lines)
|
| 108 |
+
|
| 109 |
+
if __name__ == '__main__':
|
| 110 |
+
main()
|