Repository: ku-nlp/kwja (language: python)

Issue:
Fix document example inconsistency

Resolve conflicting predictions (e.g., 体言 and 用言)

Cohesion scorer raises an assertion error while processing 950101021.knp



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 1e373f0e..4d1fd4cd 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,12 @@ Options:
 - `--output-dir, -o`: path to directory to save. Default: `./data`
 - `--save-filtered-results, -s`: whether to create an intermediate file to save the filtering results.
 
+## Build datasets for training word module
+You must have access to KyotoCorpusFull
+```shell
+./scripts/build_datasets.sh -a /path/to/activate -w ./work_dir -s /path/to/scripts -j 2 -o /path/to/output
+```
+
 ## Training and evaluation
 You can train and test the models in the following command:
 ```shell
diff --git a/configs/callbacks/word_module_writer.yaml b/configs/callbacks/word_module_writer.yaml
index a3277e04..3d34fca5 100644
--- a/configs/callbacks/word_module_writer.yaml
+++ b/configs/callbacks/word_module_writer.yaml
@@ -4,3 +4,4 @@ prediction_writer:
   pred_filename: "predict"
   model_name_or_path: ${model.model_name_or_path}
   tokenizer_kwargs: ${dataset.tokenizer_kwargs}
+  max_seq_length: ${model.max_seq_length}
diff --git a/configs/model/char_roberta_base_wwm.yaml b/configs/model/char_roberta_base_wwm.yaml
new file mode 100644
index 00000000..0b1ee3ee
--- /dev/null
+++ b/configs/model/char_roberta_base_wwm.yaml
@@ -0,0 +1,2 @@
+model_name_or_path: ${oc.env:MODEL_DIR}/char-roberta-base-wwm-step330000
+max_seq_length: 512
diff --git a/configs/model/char_roberta_large.yaml b/configs/model/char_roberta_large.yaml
new file mode 100644
index 00000000..9584fcff
--- /dev/null
+++ b/configs/model/char_roberta_large.yaml
@@ -0,0 +1,2 @@
+model_name_or_path: ${oc.env:MODEL_DIR}/char-roberta-large-step220000
+max_seq_length: 512
diff --git a/configs/word_module.debug.yaml b/configs/word_module.debug.yaml
index 4013c708..bd1d23af 100644
--- a/configs/word_module.debug.yaml
+++ b/configs/word_module.debug.yaml
@@ -14,14 +14,13 @@ defaults:
 devices: ${oc.env:GPUS,0}
 do_predict_after_train: False
 
+# for dependency parser
+k: 4
+
 # for cohesion analysis
 cases: ["ガ", "ヲ", "ニ", "ガ２"]
-bar_rels: ["ノ", "ノ？"]
-exophora_referents:
-  - 著者
-  - 読者
-  - 不特定:人
-  - 不特定:物
+bar_rels: ["ノ"]
+exophora_referents: ["著者", "読者", "不特定:人", "不特定:物"]
 cohesion_tasks: [pas_analysis, bridging, coreference]  # pas_analysis, bridging, and/or coreference
 
 # set monitor and mode for early_stopping and model_checkpoint
diff --git a/configs/word_module.yaml b/configs/word_module.yaml
index 7743be00..8f38b743 100644
--- a/configs/word_module.yaml
+++ b/configs/word_module.yaml
@@ -15,14 +15,13 @@ devices: ${oc.env:GPUS,0}
 do_predict_after_train: True
 checkpoint_path: ""
 
+# for dependency parser
+k: 4
+
 # for cohesion analysis
 cases: ["ガ", "ヲ", "ニ", "ガ２"]
-bar_rels: ["ノ", "ノ？"]
-exophora_referents:
-  - 著者
-  - 読者
-  - 不特定:人
-  - 不特定:物
+bar_rels: ["ノ"]
+exophora_referents: ["著者", "読者", "不特定:人", "不特定:物"]
 cohesion_tasks: [pas_analysis, bridging, coreference]  # pas_analysis, bridging, and/or coreference
 
 # set monitor and mode for early_stopping and model_checkpoint
diff --git a/poetry.lock b/poetry.lock
index fafdd95c..1c95e1f3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,6 +1,6 @@
 [[package]]
 name = "absl-py"
-version = "1.1.0"
+version = "1.2.0"
 description = "Abseil Python Common Libraries, see https://github.com/abseil/abseil-py."
 category = "main"
 optional = false
@@ -55,7 +55,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "atomicwrites"
-version = "1.4.0"
+version = "1.4.1"
 description = "Atomic file writes."
 category = "dev"
 optional = false
@@ -85,7 +85,7 @@ python-versions = "~=3.7"
 
 [[package]]
 name = "certifi"
-version = "2022.5.18.1"
+version = "2022.6.15"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "main"
 optional = false
@@ -93,11 +93,11 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "charset-normalizer"
-version = "2.0.12"
+version = "2.1.0"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "main"
 optional = false
-python-versions = ">=3.5.0"
+python-versions = ">=3.6.0"
 
 [package.extras]
 unicode_backport = ["unicodedata2"]
@@ -115,7 +115,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
 
 [[package]]
 name = "colorama"
-version = "0.4.4"
+version = "0.4.5"
 description = "Cross-platform colored terminal text."
 category = "main"
 optional = false
@@ -134,7 +134,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
 
 [[package]]
 name = "coverage"
-version = "6.4.1"
+version = "6.4.2"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
@@ -267,7 +267,7 @@ gitdb = ">=4.0.1,<5"
 
 [[package]]
 name = "google-auth"
-version = "2.6.6"
+version = "2.9.1"
 description = "Google Authentication Library"
 category = "main"
 optional = false
@@ -281,6 +281,7 @@ six = ">=1.9.0"
 
 [package.extras]
 aiohttp = ["requests (>=2.20.0,<3.0.0dev)", "aiohttp (>=3.6.2,<4.0.0dev)"]
+enterprise_cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"]
 pyopenssl = ["pyopenssl (>=20.0.0)"]
 reauth = ["pyu2f (>=0.1.5)"]
 
@@ -301,7 +302,7 @@ tool = ["click (>=6.0.0)"]
 
 [[package]]
 name = "grpcio"
-version = "1.46.3"
+version = "1.47.0"
 description = "HTTP/2-based RPC framework"
 category = "main"
 optional = false
@@ -311,12 +312,12 @@ python-versions = ">=3.6"
 six = ">=1.5.2"
 
 [package.extras]
-protobuf = ["grpcio-tools (>=1.46.3)"]
+protobuf = ["grpcio-tools (>=1.47.0)"]
 
 [[package]]
 name = "huggingface-hub"
-version = "0.7.0"
-description = "Client library to download and publish models on the huggingface.co hub"
+version = "0.8.1"
+description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
 category = "main"
 optional = false
 python-versions = ">=3.7.0"
@@ -330,12 +331,12 @@ tqdm = "*"
 typing-extensions = ">=3.7.4.3"
 
 [package.extras]
-all = ["pytest", "datasets", "soundfile", "black (>=22.0,<23.0)", "isort (>=5.5.4)", "flake8 (>=3.8.3)"]
-dev = ["pytest", "datasets", "soundfile", "black (>=22.0,<23.0)", "isort (>=5.5.4)", "flake8 (>=3.8.3)"]
+all = ["pytest", "pytest-cov", "datasets", "soundfile", "black (>=22.0,<23.0)", "isort (>=5.5.4)", "flake8 (>=3.8.3)"]
+dev = ["pytest", "pytest-cov", "datasets", "soundfile", "black (>=22.0,<23.0)", "isort (>=5.5.4)", "flake8 (>=3.8.3)"]
 fastai = ["toml", "fastai (>=2.4)", "fastcore (>=1.3.27)"]
 quality = ["black (>=22.0,<23.0)", "isort (>=5.5.4)", "flake8 (>=3.8.3)"]
 tensorflow = ["tensorflow", "pydot", "graphviz"]
-testing = ["pytest", "datasets", "soundfile"]
+testing = ["pytest", "pytest-cov", "datasets", "soundfile"]
 torch = ["torch"]
 
 [[package]]
@@ -361,7 +362,7 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "importlib-metadata"
-version = "4.11.4"
+version = "4.12.0"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
@@ -373,7 +374,7 @@ zipp = ">=0.5"
 [package.extras]
 docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
 perf = ["ipython"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
 
 [[package]]
 name = "iniconfig"
@@ -391,6 +392,14 @@ category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "jaconv"
+version = "0.3"
+description = "Pure-Python Japanese character interconverter for Hiragana, Katakana, Hankaku, Zenkaku and more"
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "joblib"
 version = "1.1.0"
@@ -399,13 +408,28 @@ category = "main"
 optional = false
 python-versions = ">=3.6"
 
+[[package]]
+name = "kyoto-reader"
+version = "2.5.2"
+description = "A KWDLC and KyotoCorpus parser"
+category = "main"
+optional = false
+python-versions = "!=3.7.0,!=3.7.1,>=3.7"
+
+[package.dependencies]
+jaconv = "*"
+pyknp = "*"
+
+[package.extras]
+testing = ["pytest"]
+
 [[package]]
 name = "markdown"
-version = "3.3.7"
+version = "3.4.1"
 description = "Python implementation of Markdown."
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [package.dependencies]
 importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
@@ -423,7 +447,7 @@ python-versions = ">=3.7"
 
 [[package]]
 name = "numpy"
-version = "1.22.4"
+version = "1.23.1"
 description = "NumPy is the fundamental package for array computing with Python."
 category = "main"
 optional = false
@@ -467,7 +491,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
 
 [[package]]
 name = "pandas"
-version = "1.4.2"
+version = "1.4.3"
 description = "Powerful data structures for data analysis, time series, and statistics"
 category = "main"
 optional = false
@@ -582,6 +606,17 @@ category = "main"
 optional = false
 python-versions = ">=3.6"
 
+[[package]]
+name = "pyknp"
+version = "0.6.1"
+description = "Python module for JUMAN/KNP"
+category = "main"
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+six = "*"
+
 [[package]]
 name = "pyparsing"
 version = "3.0.9"
@@ -661,7 +696,7 @@ python-versions = "*"
 
 [[package]]
 name = "pytorch-lightning"
-version = "1.6.4"
+version = "1.6.5"
 description = "PyTorch Lightning is the lightweight PyTorch wrapper for ML researchers. Scale your models. Write less boilerplate."
 category = "main"
 optional = false
@@ -681,11 +716,11 @@ tqdm = ">=4.57.0"
 typing-extensions = ">=4.0.0"
 
 [package.extras]
-all = ["matplotlib (>3.1)", "torchtext (>=0.9)", "omegaconf (>=2.0.5)", "hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.7.1)", "gcsfs (>=2021.5.0)", "rich (>=10.2.2,<10.15.0 || >=10.16.0)", "neptune-client (>=0.10.0)", "comet-ml (>=3.1.12)", "mlflow (>=1.0.0)", "test-tube (>=0.7.5)", "wandb (>=0.8.21)", "coverage (>=6.4)", "codecov (>=2.1)", "pytest (>=6.0)", "pytest-rerunfailures (>=10.2)", "mypy (>=0.920)", "flake8 (>=3.9.2)", "pre-commit (>=1.0)", "pytest-forked", "cloudpickle (>=1.3)", "scikit-learn (>0.22.1)", "onnxruntime", "pandas", "torchvision (>=0.9)", "gym[classic_control] (>=0.17.0)", "ipython", "fairscale (>=0.4.5)", "deepspeed", "horovod (>=0.21.2,!=0.24.0)", "hivemind (>=1.0.1)"]
+all = ["matplotlib (>3.1)", "torchtext (>=0.9)", "omegaconf (>=2.0.5)", "hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.7.1)", "gcsfs (>=2021.5.0)", "rich (>=10.2.2,!=10.15.0.a)", "neptune-client (>=0.10.0)", "comet-ml (>=3.1.12)", "mlflow (>=1.0.0)", "test-tube (>=0.7.5)", "wandb (>=0.8.21)", "coverage (>=6.4)", "codecov (>=2.1)", "pytest (>=6.0)", "pytest-rerunfailures (>=10.2)", "mypy (>=0.920)", "flake8 (>=3.9.2)", "pre-commit (>=1.0)", "pytest-forked", "cloudpickle (>=1.3)", "scikit-learn (>0.22.1)", "onnxruntime", "pandas", "torchvision (>=0.9)", "gym[classic_control] (>=0.17.0)", "ipython", "fairscale (>=0.4.5)", "deepspeed", "horovod (>=0.21.2,!=0.24.0)", "hivemind (>=1.0.1)"]
 deepspeed = ["deepspeed"]
-dev = ["matplotlib (>3.1)", "torchtext (>=0.9)", "omegaconf (>=2.0.5)", "hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.7.1)", "gcsfs (>=2021.5.0)", "rich (>=10.2.2,<10.15.0 || >=10.16.0)", "neptune-client (>=0.10.0)", "comet-ml (>=3.1.12)", "mlflow (>=1.0.0)", "test-tube (>=0.7.5)", "wandb (>=0.8.21)", "coverage (>=6.4)", "codecov (>=2.1)", "pytest (>=6.0)", "pytest-rerunfailures (>=10.2)", "mypy (>=0.920)", "flake8 (>=3.9.2)", "pre-commit (>=1.0)", "pytest-forked", "cloudpickle (>=1.3)", "scikit-learn (>0.22.1)", "onnxruntime", "pandas"]
+dev = ["matplotlib (>3.1)", "torchtext (>=0.9)", "omegaconf (>=2.0.5)", "hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.7.1)", "gcsfs (>=2021.5.0)", "rich (>=10.2.2,!=10.15.0.a)", "neptune-client (>=0.10.0)", "comet-ml (>=3.1.12)", "mlflow (>=1.0.0)", "test-tube (>=0.7.5)", "wandb (>=0.8.21)", "coverage (>=6.4)", "codecov (>=2.1)", "pytest (>=6.0)", "pytest-rerunfailures (>=10.2)", "mypy (>=0.920)", "flake8 (>=3.9.2)", "pre-commit (>=1.0)", "pytest-forked", "cloudpickle (>=1.3)", "scikit-learn (>0.22.1)", "onnxruntime", "pandas"]
 examples = ["torchvision (>=0.9)", "gym[classic_control] (>=0.17.0)", "ipython"]
-extra = ["matplotlib (>3.1)", "torchtext (>=0.9)", "omegaconf (>=2.0.5)", "hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.7.1)", "gcsfs (>=2021.5.0)", "rich (>=10.2.2,<10.15.0 || >=10.16.0)"]
+extra = ["matplotlib (>3.1)", "torchtext (>=0.9)", "omegaconf (>=2.0.5)", "hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.7.1)", "gcsfs (>=2021.5.0)", "rich (>=10.2.2,!=10.15.0.a)"]
 fairscale = ["fairscale (>=0.4.5)"]
 hivemind = ["hivemind (>=1.0.1)"]
 horovod = ["horovod (>=0.21.2,!=0.24.0)"]
@@ -711,7 +746,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "regex"
-version = "2022.6.2"
+version = "2022.7.9"
 description = "Alternative regular expression module, to replace re."
 category = "main"
 optional = false
@@ -719,21 +754,21 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "requests"
-version = "2.27.1"
+version = "2.28.1"
 description = "Python HTTP for Humans."
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+python-versions = ">=3.7, <4"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
-idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
+charset-normalizer = ">=2,<3"
+idna = ">=2.5,<4"
 urllib3 = ">=1.21.1,<1.27"
 
 [package.extras]
-socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
-use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "requests-oauthlib"
@@ -752,7 +787,7 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
 
 [[package]]
 name = "rhoknp"
-version = "0.1.1"
+version = "0.2.0"
 description = "Yet another Python binding for Juman++/KNP"
 category = "main"
 optional = false
@@ -763,11 +798,11 @@ develop = false
 type = "git"
 url = "https://github.com/ku-nlp/rhoknp.git"
 reference = "develop"
-resolved_reference = "b18316259d68e16637b5ddb492c81813a7672924"
+resolved_reference = "92e37582b87349f71f309e775a5e35e3bf9a8db0"
 
 [[package]]
 name = "rich"
-version = "12.4.4"
+version = "12.5.1"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
 category = "main"
 optional = false
@@ -782,7 +817,7 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
 
 [[package]]
 name = "rsa"
-version = "4.8"
+version = "4.9"
 description = "Pure-Python RSA implementation"
 category = "main"
 optional = false
@@ -832,7 +867,7 @@ python-versions = "*"
 
 [[package]]
 name = "sentry-sdk"
-version = "1.5.12"
+version = "1.8.0"
 description = "Python client for Sentry (https://sentry.io)"
 category = "main"
 optional = false
@@ -850,6 +885,7 @@ celery = ["celery (>=3)"]
 chalice = ["chalice (>=1.16.0)"]
 django = ["django (>=1.8)"]
 falcon = ["falcon (>=1.4)"]
+fastapi = ["fastapi (>=0.79.0)"]
 flask = ["flask (>=0.11)", "blinker (>=1.1)"]
 httpx = ["httpx (>=0.16.0)"]
 pure_eval = ["pure-eval", "executing", "asttokens"]
@@ -858,6 +894,7 @@ quart = ["quart (>=0.16.1)", "blinker (>=1.1)"]
 rq = ["rq (>=0.6)"]
 sanic = ["sanic (>=0.8)"]
 sqlalchemy = ["sqlalchemy (>=1.2)"]
+starlette = ["starlette (>=0.19.1)"]
 tornado = ["tornado (>=5)"]
 
 [[package]]
@@ -985,7 +1022,7 @@ typing-extensions = "*"
 
 [[package]]
 name = "torchmetrics"
-version = "0.9.0"
+version = "0.9.2"
 description = "PyTorch native Metrics"
 category = "main"
 optional = false
@@ -997,14 +1034,14 @@ packaging = "*"
 torch = ">=1.3.1"
 
 [package.extras]
-all = ["pystoi", "pycocotools", "torchvision (>=0.8)", "sphinx-copybutton (>=0.3)", "myst-parser", "sphinxcontrib-fulltoc (>=1.0)", "sphinx (>=4.0)", "sphinxcontrib-mockautodoc", "sphinx-togglebutton (>=0.2)", "docutils (>=0.16)", "pandoc (>=1.0)", "nbsphinx (>=0.8)", "sphinx-paramlinks (>=0.5.1)", "sphinx-autodoc-typehints (>=1.0)", "torch-fidelity", "torchvision", "lpips", "scipy", "pytorch-lightning (>=1.5)", "pytest (>=6.0.0,<7.0.0)", "phmdoctest (>=1.1.1)", "psutil", "pytest-cov (>2.10)", "pypesq", "check-manifest", "sacrebleu (>=2.0.0)", "pytest-timeout", "mypy (>=0.790)", "scikit-image (>0.17.1)", "transformers (>=4.0)", "pytorch-msssim", "cloudpickle (>=1.3)", "twine (>=3.2)", "codecov (>=2.1)", "pre-commit (>=1.0)", "huggingface-hub (<0.7)", "coverage (>5.2)", "requests", "torch-complex", "jiwer (>=2.3.0)", "bert-score (==0.3.10)", "mir-eval (>=0.6)", "fast-bss-eval (>=0.1.0)", "fire", "rouge-score (>=0.0.4)", "scikit-learn (>1.0,<1.1.1)", "pytest-doctestplus (>=0.9.0)", "tqdm (>=4.41.0)", "nltk (>=3.6)", "regex (>=2021.9.24)"]
+all = ["pystoi", "torchvision (>=0.8)", "pycocotools", "sphinx-copybutton (>=0.3)", "nbsphinx (>=0.8)", "docutils (>=0.16)", "sphinxcontrib-fulltoc (>=1.0)", "sphinx-paramlinks (>=0.5.1)", "sphinx-autodoc-typehints (>=1.0)", "sphinx (>=4.0,<5.0)", "myst-parser", "sphinx-togglebutton (>=0.2)", "pandoc (>=1.0)", "sphinxcontrib-mockautodoc", "torchvision", "torch-fidelity", "lpips", "scipy", "pytorch-lightning (>=1.5)", "mypy (>=0.790)", "transformers (>=4.0)", "pytorch-msssim", "scikit-image (>0.17.1)", "fast-bss-eval (>=0.1.0)", "rouge-score (>=0.0.4)", "torch-complex", "bert-score (==0.3.10)", "cloudpickle (>=1.3)", "jiwer (>=2.3.0)", "pypesq", "coverage (>5.2)", "codecov (>=2.1)", "huggingface-hub (<0.7)", "sacrebleu (>=2.0.0)", "pytest-cov (>2.10)", "psutil", "pre-commit (>=1.0)", "requests", "pytest (>=6.0.0,<7.0.0)", "twine (>=3.2)", "scikit-learn (>1.0,<1.1.1)", "pytest-doctestplus (>=0.9.0)", "pytest-timeout", "phmdoctest (>=1.1.1)", "check-manifest", "mir-eval (>=0.6)", "fire", "nltk (>=3.6)", "regex (>=2021.9.24)", "tqdm (>=4.41.0)"]
 audio = ["pystoi"]
-detection = ["pycocotools", "torchvision (>=0.8)"]
-docs = ["sphinx-copybutton (>=0.3)", "myst-parser", "sphinxcontrib-fulltoc (>=1.0)", "sphinx (>=4.0)", "sphinxcontrib-mockautodoc", "sphinx-togglebutton (>=0.2)", "docutils (>=0.16)", "pandoc (>=1.0)", "nbsphinx (>=0.8)", "sphinx-paramlinks (>=0.5.1)", "sphinx-autodoc-typehints (>=1.0)"]
-image = ["torch-fidelity", "torchvision", "lpips", "scipy"]
+detection = ["torchvision (>=0.8)", "pycocotools"]
+docs = ["sphinx-copybutton (>=0.3)", "nbsphinx (>=0.8)", "docutils (>=0.16)", "sphinxcontrib-fulltoc (>=1.0)", "sphinx-paramlinks (>=0.5.1)", "sphinx-autodoc-typehints (>=1.0)", "sphinx (>=4.0,<5.0)", "myst-parser", "sphinx-togglebutton (>=0.2)", "pandoc (>=1.0)", "sphinxcontrib-mockautodoc"]
+image = ["torchvision", "torch-fidelity", "lpips", "scipy"]
 integrate = ["pytorch-lightning (>=1.5)"]
-test = ["pytest (>=6.0.0,<7.0.0)", "pycocotools", "phmdoctest (>=1.1.1)", "psutil", "pytest-cov (>2.10)", "pypesq", "check-manifest", "sacrebleu (>=2.0.0)", "pytest-timeout", "mypy (>=0.790)", "scikit-image (>0.17.1)", "transformers (>=4.0)", "pytorch-msssim", "cloudpickle (>=1.3)", "twine (>=3.2)", "codecov (>=2.1)", "pre-commit (>=1.0)", "huggingface-hub (<0.7)", "coverage (>5.2)", "requests", "torch-complex", "jiwer (>=2.3.0)", "bert-score (==0.3.10)", "mir-eval (>=0.6)", "fast-bss-eval (>=0.1.0)", "fire", "rouge-score (>=0.0.4)", "scikit-learn (>1.0,<1.1.1)", "pytest-doctestplus (>=0.9.0)"]
-text = ["tqdm (>=4.41.0)", "nltk (>=3.6)", "regex (>=2021.9.24)"]
+test = ["mypy (>=0.790)", "transformers (>=4.0)", "pytorch-msssim", "scikit-image (>0.17.1)", "fast-bss-eval (>=0.1.0)", "pycocotools", "rouge-score (>=0.0.4)", "torch-complex", "bert-score (==0.3.10)", "cloudpickle (>=1.3)", "jiwer (>=2.3.0)", "pypesq", "coverage (>5.2)", "codecov (>=2.1)", "huggingface-hub (<0.7)", "sacrebleu (>=2.0.0)", "pytest-cov (>2.10)", "psutil", "pre-commit (>=1.0)", "requests", "pytest (>=6.0.0,<7.0.0)", "twine (>=3.2)", "scikit-learn (>1.0,<1.1.1)", "pytest-doctestplus (>=0.9.0)", "pytest-timeout", "phmdoctest (>=1.1.1)", "check-manifest", "mir-eval (>=0.6)", "fire"]
+text = ["nltk (>=3.6)", "regex (>=2021.9.24)", "tqdm (>=4.41.0)"]
 
 [[package]]
 name = "tqdm"
@@ -1025,7 +1062,7 @@ telegram = ["requests"]
 
 [[package]]
 name = "transformers"
-version = "4.19.2"
+version = "4.19.4"
 description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow"
 category = "main"
 optional = false
@@ -1043,15 +1080,15 @@ tokenizers = ">=0.11.1,<0.11.3 || >0.11.3,<0.13"
 tqdm = ">=4.27"
 
 [package.extras]
-all = ["tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx", "torch (>=1.0)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "flax (>=0.3.5)", "optax (>=0.0.8)", "sentencepiece (>=0.1.91,!=0.1.92)", "protobuf", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer", "pillow", "optuna", "ray", "sigopt", "timm", "codecarbon (==1.2.0)"]
+all = ["tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx", "torch (>=1.0)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "flax (>=0.3.5)", "optax (>=0.0.8)", "sentencepiece (>=0.1.91,!=0.1.92)", "protobuf (<=3.20.1)", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer", "pillow", "optuna", "ray", "sigopt", "timm", "codecarbon (==1.2.0)"]
 audio = ["librosa", "pyctcdecode (>=0.3.0)", "phonemizer"]
 codecarbon = ["codecarbon (==1.2.0)"]
 deepspeed = ["deepspeed (>=0.6.4)"]
-deepspeed-testing = ["deepspeed (>=0.6.4)", "pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)", "optuna"]
-dev = ["tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx", "torch (>=1.0)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "flax (>=0.3.5)", "optax (>=0.0.8)", "sentencepiece (>=0.1.91,!=0.1.92)", "protobuf", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer", "pillow", "optuna", "ray", "sigopt", "timm", "codecarbon (==1.2.0)", "pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)", "isort (>=5.5.4)", "flake8 (>=3.8.3)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "unidic-lite (>=1.0.7)", "unidic (>=1.0.2)", "hf-doc-builder", "scikit-learn"]
-dev-tensorflow = ["pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)", "tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx", "sentencepiece (>=0.1.91,!=0.1.92)", "protobuf", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "pillow", "isort (>=5.5.4)", "flake8 (>=3.8.3)", "hf-doc-builder", "scikit-learn", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer"]
-dev-torch = ["pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)", "torch (>=1.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "protobuf", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer", "pillow", "optuna", "ray", "sigopt", "timm", "codecarbon (==1.2.0)", "isort (>=5.5.4)", "flake8 (>=3.8.3)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "unidic-lite (>=1.0.7)", "unidic (>=1.0.2)", "hf-doc-builder", "scikit-learn", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"]
-docs = ["tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx", "torch (>=1.0)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "flax (>=0.3.5)", "optax (>=0.0.8)", "sentencepiece (>=0.1.91,!=0.1.92)", "protobuf", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer", "pillow", "optuna", "ray", "sigopt", "timm", "codecarbon (==1.2.0)", "hf-doc-builder"]
+deepspeed-testing = ["deepspeed (>=0.6.4)", "pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "protobuf (<=3.20.1)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)", "optuna"]
+dev = ["tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx", "torch (>=1.0)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "flax (>=0.3.5)", "optax (>=0.0.8)", "sentencepiece (>=0.1.91,!=0.1.92)", "protobuf (<=3.20.1)", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer", "pillow", "optuna", "ray", "sigopt", "timm", "codecarbon (==1.2.0)", "pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)", "isort (>=5.5.4)", "flake8 (>=3.8.3)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "unidic-lite (>=1.0.7)", "unidic (>=1.0.2)", "hf-doc-builder", "scikit-learn"]
+dev-tensorflow = ["pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "protobuf (<=3.20.1)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)", "tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "pillow", "isort (>=5.5.4)", "flake8 (>=3.8.3)", "hf-doc-builder", "scikit-learn", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer"]
+dev-torch = ["pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "protobuf (<=3.20.1)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)", "torch (>=1.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer", "pillow", "optuna", "ray", "sigopt", "timm", "codecarbon (==1.2.0)", "isort (>=5.5.4)", "flake8 (>=3.8.3)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "unidic-lite (>=1.0.7)", "unidic (>=1.0.2)", "hf-doc-builder", "scikit-learn", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"]
+docs = ["tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx", "torch (>=1.0)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "flax (>=0.3.5)", "optax (>=0.0.8)", "sentencepiece (>=0.1.91,!=0.1.92)", "protobuf (<=3.20.1)", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer", "pillow", "optuna", "ray", "sigopt", "timm", "codecarbon (==1.2.0)", "hf-doc-builder"]
 docs_specific = ["hf-doc-builder"]
 fairscale = ["fairscale (>0.3)"]
 flax = ["jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "flax (>=0.3.5)", "optax (>=0.0.8)"]
@@ -1067,12 +1104,12 @@ quality = ["black (>=22.0,<23.0)", "isort (>=5.5.4)", "flake8 (>=3.8.3)", "GitPy
 ray = ["ray"]
 retrieval = ["faiss-cpu", "datasets"]
 sagemaker = ["sagemaker (>=2.31.0)"]
-sentencepiece = ["sentencepiece (>=0.1.91,!=0.1.92)", "protobuf"]
+sentencepiece = ["sentencepiece (>=0.1.91,!=0.1.92)", "protobuf (<=3.20.1)"]
 serving = ["pydantic", "uvicorn", "fastapi", "starlette"]
 sigopt = ["sigopt"]
 sklearn = ["scikit-learn"]
 speech = ["torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer"]
-testing = ["pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)"]
+testing = ["pytest", "pytest-xdist", "timeout-decorator", "parameterized", "psutil", "datasets", "pytest-timeout", "black (>=22.0,<23.0)", "sacrebleu (>=1.4.12,<2.0.0)", "rouge-score", "nltk", "GitPython (<3.1.19)", "hf-doc-builder (>=0.3.0)", "protobuf (<=3.20.1)", "sacremoses", "rjieba", "faiss-cpu", "cookiecutter (==1.7.3)"]
 tf = ["tensorflow (>=2.3)", "onnxconverter-common", "tf2onnx"]
 tf-cpu = ["tensorflow-cpu (>=2.3)", "onnxconverter-common", "tf2onnx"]
 tf-speech = ["librosa", "pyctcdecode (>=0.3.0)", "phonemizer"]
@@ -1080,12 +1117,12 @@ timm = ["timm"]
 tokenizers = ["tokenizers (>=0.11.1,!=0.11.3,<0.13)"]
 torch = ["torch (>=1.0)"]
 torch-speech = ["torchaudio", "librosa", "pyctcdecode (>=0.3.0)", "phonemizer"]
-torchhub = ["filelock", "huggingface-hub (>=0.1.0,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "torch (>=1.0)", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "tqdm (>=4.27)"]
+torchhub = ["filelock", "huggingface-hub (>=0.1.0,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf (<=3.20.1)", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "torch (>=1.0)", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "tqdm (>=4.27)"]
 vision = ["pillow"]
 
 [[package]]
 name = "typer"
-version = "0.4.1"
+version = "0.4.2"
 description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
 category = "main"
 optional = false
@@ -1096,13 +1133,13 @@ click = ">=7.1.1,<9.0.0"
 
 [package.extras]
 all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"]
-dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"]
+dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"]
 doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"]
 test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)"]
 
 [[package]]
 name = "typing-extensions"
-version = "4.2.0"
+version = "4.3.0"
 description = "Backported and Experimental Type Hints for Python 3.7+"
 category = "main"
 optional = false
@@ -1110,11 +1147,11 @@ python-versions = ">=3.7"
 
 [[package]]
 name = "urllib3"
-version = "1.26.9"
+version = "1.26.10"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
 
 [package.extras]
 brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
@@ -1123,7 +1160,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
 [[package]]
 name = "wandb"
-version = "0.12.17"
+version = "0.12.21"
 description = "A CLI and library for interacting with the Weights and Biases API."
 category = "main"
 optional = false
@@ -1137,7 +1174,6 @@ pathtools = "*"
 promise = ">=2.0,<3"
 protobuf = ">=3.12.0,<4.0dev"
 psutil = ">=5.0.0"
-python-dateutil = ">=2.6.1"
 PyYAML = "*"
 requests = ">=2.0.0,<3"
 sentry-sdk = ">=1.0.0"
@@ -1153,7 +1189,8 @@ grpc = ["grpcio (>=1.27.2)"]
 kubeflow = ["kubernetes", "minio", "google-cloud-storage", "sh"]
 launch = ["nbconvert", "nbformat", "chardet", "iso8601", "typing-extensions", "boto3", "google-cloud-storage", "kubernetes"]
 media = ["numpy", "moviepy", "pillow", "bokeh", "soundfile", "plotly", "rdkit-pypi"]
-sweeps = ["numpy (>=1.15,<1.21)", "scipy (>=1.5.4)", "pyyaml", "scikit-learn (==0.24.1)", "jsonschema (>=3.2.0)", "jsonref (>=0.2)", "pydantic (>=1.8.2)"]
+models = ["cloudpickle"]
+sweeps = ["sweeps (>=0.1.0)"]
 
 [[package]]
 name = "werkzeug"
@@ -1180,26 +1217,23 @@ multidict = ">=4.0"
 
 [[package]]
 name = "zipp"
-version = "3.8.0"
+version = "3.8.1"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = ">=3.9,<3.11"
-content-hash = "92226622769f65fed22980030963ee7280d620fbaff3cb16128065247d01158d"
+content-hash = "ce58d0c195933777261b829b08456a8205d65dac62eaa44a6d97c16280f0790b"
 
 [metadata.files]
-absl-py = [
-    {file = "absl-py-1.1.0.tar.gz", hash = "sha256:3aa39f898329c2156ff525dfa69ce709e42d77aab18bf4917719d6f260aa6a08"},
-    {file = "absl_py-1.1.0-py3-none-any.whl", hash = "sha256:db97287655e30336938f8058d2c81ed2be6af1d9b6ebbcd8df1080a6c7fcd24e"},
-]
+absl-py = []
 aiohttp = [
     {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
     {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
@@ -1285,10 +1319,7 @@ async-timeout = [
     {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
     {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
 ]
-atomicwrites = [
-    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
-    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
-]
+atomicwrites = []
 attrs = [
     {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
     {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
@@ -1298,68 +1329,23 @@ cachetools = [
     {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"},
 ]
 certifi = [
-    {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"},
-    {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"},
-]
-charset-normalizer = [
-    {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
-    {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
+    {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
+    {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
 ]
+charset-normalizer = []
 click = [
     {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
     {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
 ]
 colorama = [
-    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
-    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+    {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
+    {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
 ]
 commonmark = [
     {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
     {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
 ]
-coverage = [
-    {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"},
-    {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"},
-    {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"},
-    {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"},
-    {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"},
-    {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"},
-    {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"},
-    {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"},
-    {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"},
-    {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"},
-    {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"},
-    {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"},
-    {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"},
-    {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"},
-    {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"},
-    {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"},
-    {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"},
-]
+coverage = []
 cython = [
     {file = "Cython-0.29.30-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5cb144728a335d7a7fd0a61dff6abb7a9aeff9acd46d50b886b7d9a95bb7311"},
     {file = "Cython-0.29.30-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d52d5733dcb144deca8985f0a197c19cf71e6bd6bd9d8034f3f67b2dea68d12b"},
@@ -1532,74 +1518,13 @@ gitpython = [
     {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
     {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
 ]
-google-auth = [
-    {file = "google-auth-2.6.6.tar.gz", hash = "sha256:1ba4938e032b73deb51e59c4656a00e0939cf0b1112575099f136babb4563312"},
-    {file = "google_auth-2.6.6-py2.py3-none-any.whl", hash = "sha256:349ac49b18b01019453cc99c11c92ed772739778c92f184002b7ab3a5b7ac77d"},
-]
+google-auth = []
 google-auth-oauthlib = [
     {file = "google-auth-oauthlib-0.4.6.tar.gz", hash = "sha256:a90a072f6993f2c327067bf65270046384cda5a8ecb20b94ea9a687f1f233a7a"},
     {file = "google_auth_oauthlib-0.4.6-py2.py3-none-any.whl", hash = "sha256:3f2a6e802eebbb6fb736a370fbf3b055edcb6b52878bf2f26330b5e041316c73"},
 ]
-grpcio = [
-    {file = "grpcio-1.46.3-cp310-cp310-linux_armv7l.whl", hash = "sha256:4c05dbc164c2d3015109292ffeed68292807a6cb1225f9a36699bf2166634908"},
-    {file = "grpcio-1.46.3-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c6a460b6aaf43428d48fececad864cc562458b944df80568e490d985d8576292"},
-    {file = "grpcio-1.46.3-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:707b85fa0cf606a9ab02246bd3142c76e154f1c30f00f7346b2afa3d0b315d5a"},
-    {file = "grpcio-1.46.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c63e7c61c0b06f838e8f45ffd3a7c68a520c4c026b2e0e8b1ad29c456d0f859"},
-    {file = "grpcio-1.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6fe85e5873d9784ab82cf261d9fc07ed67a4459ba69fbe1187ef8b8e3d9e30e"},
-    {file = "grpcio-1.46.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df980c4901a92ca649e18036ff67c7c8cad239b2759c2472694f7ab0f0b4ffb9"},
-    {file = "grpcio-1.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b59982e405159385d5796aa1e0817ec83affb3eb4c2a5b7ca39413d17d7e332"},
-    {file = "grpcio-1.46.3-cp310-cp310-win32.whl", hash = "sha256:6d51fa98bd40d4593f819a3fec8a078a192958d24f84c3daf15b5ad7705d4c48"},
-    {file = "grpcio-1.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:e9bba429eb743471715e6dadf006a70a77cb6afb065aa4a6eaa9efd76b09e336"},
-    {file = "grpcio-1.46.3-cp36-cp36m-linux_armv7l.whl", hash = "sha256:a898b0f13bda2dfe786952cc1ea705762fa6c3ae799b4bb0525d7821605ae968"},
-    {file = "grpcio-1.46.3-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:9014aee70e29911008d2f388011cabf2c7fe4fe29918ce5f71513a660494069a"},
-    {file = "grpcio-1.46.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9c97106134de70f8323b12738ac0adf0615688b69253002910d0c5d42d202a77"},
-    {file = "grpcio-1.46.3-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d41ea8efb87b1ae4e576b13d94f2b470297a1495ae6b2c9d1047952731bf168f"},
-    {file = "grpcio-1.46.3-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:ab18e85082003d7883a4d069065436e61cb27c2c2150e7965ce93658f17bc8da"},
-    {file = "grpcio-1.46.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:307ff1d6237d5c383196660a12db021c20280227f9f4423d88d6b2ab20c8b1d0"},
-    {file = "grpcio-1.46.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c9106ef35239767b3aa9dc1a79856ad499655f853fca9f92f9dd3182d646627"},
-    {file = "grpcio-1.46.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e0ae8e8523308bf7ab0b7d6aa686011de59b19fb06abb253f302d0b5da2a5905"},
-    {file = "grpcio-1.46.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4fd0aa30a938893060defd5f222604461db55f9a81a028b154479b91deac7074"},
-    {file = "grpcio-1.46.3-cp36-cp36m-win32.whl", hash = "sha256:f7637b55662e56a64c07846bc0d2da6232a6e893b22c39790f2e41d03ac1a826"},
-    {file = "grpcio-1.46.3-cp36-cp36m-win_amd64.whl", hash = "sha256:97801afa96a819f911d030b490dbea95b246de02433bac69c5acf150081686e4"},
-    {file = "grpcio-1.46.3-cp37-cp37m-linux_armv7l.whl", hash = "sha256:3585a6fa3d97fc8f030bbf0e88185b5eb345a340f6732e165d5c22df54de5bc6"},
-    {file = "grpcio-1.46.3-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:dc6d15cbcceaebaacf2994280ed1c01d42b5772059b30afd8a76152e9d23daa4"},
-    {file = "grpcio-1.46.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e0486485d59d5865149010966ef3df99c5df97ab8b01f10e26f8759d6e10fafc"},
-    {file = "grpcio-1.46.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5210ec7a1638daa61da16487fbfafb3dbb7b8cd44382d9262316bbb58a5b1cf7"},
-    {file = "grpcio-1.46.3-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:e278fa30d2b5652f7e43970c86ad34c639146443553678b746909aae204924dc"},
-    {file = "grpcio-1.46.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d4148f1f76516b01cccf2273b45bc706847f1560ccb55aa6e29df851e9ca8cc"},
-    {file = "grpcio-1.46.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01f3f7a6cdb111cf276ffff9c892fa32624e03999bac809d3f3d8321d98b6855"},
-    {file = "grpcio-1.46.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:91aaccbe1c035ad2bcd1b8a25cebd11839070eb70fb6573e9d0197ddbca5d96b"},
-    {file = "grpcio-1.46.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:26136c19d96e2138f04412101f3730d66f5f1515dc912ac0d415587c8208d826"},
-    {file = "grpcio-1.46.3-cp37-cp37m-win32.whl", hash = "sha256:a8f40dafcdc3e0e378387953528eaf4e35758161f3b10d96199f12b11afbe2c2"},
-    {file = "grpcio-1.46.3-cp37-cp37m-win_amd64.whl", hash = "sha256:a6bb52df85a4bd6d3bad16b4e7cc43efe95469b74a856c87a2c5bef496c9147f"},
-    {file = "grpcio-1.46.3-cp38-cp38-linux_armv7l.whl", hash = "sha256:2334ceeab4084e80433693451452cba26afc1607a7974133af3b3635fc8aa935"},
-    {file = "grpcio-1.46.3-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:2c96a6103caec84985bb2cffac2b261f8cac2641e7a70d4b43b7d08754a6cfe7"},
-    {file = "grpcio-1.46.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7a39d39da8855b03be2d7348387986bab6a322031fcc8b04fa5e72355e7b13a1"},
-    {file = "grpcio-1.46.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4caf87a06de88e3611a4610c57ef55b78801843d1f5a9e5fd6b75e887dad3340"},
-    {file = "grpcio-1.46.3-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:ffbbb228e6fc6f85b34aac428eb76b4fc6591d771e487ce46eb16b4b7e18b91d"},
-    {file = "grpcio-1.46.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c89ae010c57333dd3c692e0892199a59df1ddfd467cdfea31f98331d0e8cf87"},
-    {file = "grpcio-1.46.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34b206cdf78dd1c14d93e10e7308750c36b4e6754d579895cba74341875e2fb5"},
-    {file = "grpcio-1.46.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a19b3ecdb8ddf60e4b034def27636065e49ac1ee3c85854a16353cf52c2afd83"},
-    {file = "grpcio-1.46.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aac6e66acae82be5c99a0a40ab8f5733d7df76a04f242cf42ecc34cfb1e947bd"},
-    {file = "grpcio-1.46.3-cp38-cp38-win32.whl", hash = "sha256:aff6d961d6bc5e34e12e148383671f8da5d17e47ed606ec15f483def3053b206"},
-    {file = "grpcio-1.46.3-cp38-cp38-win_amd64.whl", hash = "sha256:71d46c2f3c0512bac3d658af3193e3d645c96123af56bd07a8416474c69df2cf"},
-    {file = "grpcio-1.46.3-cp39-cp39-linux_armv7l.whl", hash = "sha256:5969f63f3cf92538f83f26949d393d9fc59de670f47cf7c2a0e1e0d30b770294"},
-    {file = "grpcio-1.46.3-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5f8134d4a7e76c8c6644bd3ce728b9894933575155d02c09922986d5d8d6e48c"},
-    {file = "grpcio-1.46.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53fff69fd4d315adddda226e7b71804d1f12adf3a4162126dc520725624a483a"},
-    {file = "grpcio-1.46.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3af2cc4e41f87d3b57f624b1b14321c1d0f030b191da60f9eeeda5448d83240c"},
-    {file = "grpcio-1.46.3-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:5fb7779ae01c20c4fad5831e98003b3f036acfe6b77697d6a9baa0f9a7f14daf"},
-    {file = "grpcio-1.46.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56636ebf8db63ba50d272dfd73c92538950525120311676246f8f6a81b0aa144"},
-    {file = "grpcio-1.46.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a5012ba00cf8b7ce9e6ac2312ace0b0e16fe9502c18340c8c3ecb734a759831"},
-    {file = "grpcio-1.46.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:be1679d814a292a701f45df324e25b060435dd13159e9b08a16e2a2396c4391c"},
-    {file = "grpcio-1.46.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4faaba7db078a0001a8c1a4370d56dc454c03b4613b6acec01f14b90c8dd03cf"},
-    {file = "grpcio-1.46.3-cp39-cp39-win32.whl", hash = "sha256:f5c6393fa645183ae858ebfbf72ab94e7ebafb5cd849dcf4ae8c53a83cce4e24"},
-    {file = "grpcio-1.46.3-cp39-cp39-win_amd64.whl", hash = "sha256:158b90d4f1354f40e435f4c866057acc29a4364b214c31049c8b8c903646fbab"},
-    {file = "grpcio-1.46.3.tar.gz", hash = "sha256:4b8fd8b1cd553635274b83cd984f0755e6779886eca53c1c71d48215962eb689"},
-]
-huggingface-hub = [
-    {file = "huggingface_hub-0.7.0-py3-none-any.whl", hash = "sha256:fd448fd0b738d803411c79bdf9f12f0ba171fecd24a59edf88c1391b473bc2c0"},
-    {file = "huggingface_hub-0.7.0.tar.gz", hash = "sha256:8154dc2fad84b32a4bca18372a647d9381ed8550a80b11050758357b8fcea639"},
-]
+grpcio = []
+huggingface-hub = []
 hydra-core = [
     {file = "hydra-core-1.2.0.tar.gz", hash = "sha256:4990721ce4ac69abafaffee566d6b63a54faa6501ecce65b338d3251446ff634"},
     {file = "hydra_core-1.2.0-py3-none-any.whl", hash = "sha256:b6614fd6d6a97a9499f7ddbef02c9dd38f2fec6a9bc83c10e248db1dae50a528"},
@@ -1608,10 +1533,7 @@ idna = [
     {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
     {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
 ]
-importlib-metadata = [
-    {file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"},
-    {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"},
-]
+importlib-metadata = []
 iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
@@ -1619,14 +1541,15 @@ iniconfig = [
 ipadic = [
     {file = "ipadic-1.0.0.tar.gz", hash = "sha256:f5923d31eca6131acaaf18ed28d8998665b1347b640d3a6476f64650e9a71c07"},
 ]
+jaconv = [
+    {file = "jaconv-0.3.tar.gz", hash = "sha256:cc70c796c19a6765598c03eac59d1399a555a9a8839cc70e540ec26f0ec3e66e"},
+]
 joblib = [
     {file = "joblib-1.1.0-py2.py3-none-any.whl", hash = "sha256:f21f109b3c7ff9d95f8387f752d0d9c34a02aa2f7060c2135f465da0e5160ff6"},
     {file = "joblib-1.1.0.tar.gz", hash = "sha256:4158fcecd13733f8be669be0683b96ebdbbd38d23559f54dca7205aea1bf1e35"},
 ]
-markdown = [
-    {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"},
-    {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"},
-]
+kyoto-reader = []
+markdown = []
 multidict = [
     {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
     {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
@@ -1688,30 +1611,7 @@ multidict = [
     {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
     {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
 ]
-numpy = [
-    {file = "numpy-1.22.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:ba9ead61dfb5d971d77b6c131a9dbee62294a932bf6a356e48c75ae684e635b3"},
-    {file = "numpy-1.22.4-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1ce7ab2053e36c0a71e7a13a7475bd3b1f54750b4b433adc96313e127b870887"},
-    {file = "numpy-1.22.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7228ad13744f63575b3a972d7ee4fd61815b2879998e70930d4ccf9ec721dce0"},
-    {file = "numpy-1.22.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43a8ca7391b626b4c4fe20aefe79fec683279e31e7c79716863b4b25021e0e74"},
-    {file = "numpy-1.22.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a911e317e8c826ea632205e63ed8507e0dc877dcdc49744584dfc363df9ca08c"},
-    {file = "numpy-1.22.4-cp310-cp310-win32.whl", hash = "sha256:9ce7df0abeabe7fbd8ccbf343dc0db72f68549856b863ae3dd580255d009648e"},
-    {file = "numpy-1.22.4-cp310-cp310-win_amd64.whl", hash = "sha256:3e1ffa4748168e1cc8d3cde93f006fe92b5421396221a02f2274aab6ac83b077"},
-    {file = "numpy-1.22.4-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:59d55e634968b8f77d3fd674a3cf0b96e85147cd6556ec64ade018f27e9479e1"},
-    {file = "numpy-1.22.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c1d937820db6e43bec43e8d016b9b3165dcb42892ea9f106c70fb13d430ffe72"},
-    {file = "numpy-1.22.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4c5d5eb2ec8da0b4f50c9a843393971f31f1d60be87e0fb0917a49133d257d6"},
-    {file = "numpy-1.22.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64f56fc53a2d18b1924abd15745e30d82a5782b2cab3429aceecc6875bd5add0"},
-    {file = "numpy-1.22.4-cp38-cp38-win32.whl", hash = "sha256:fb7a980c81dd932381f8228a426df8aeb70d59bbcda2af075b627bbc50207cba"},
-    {file = "numpy-1.22.4-cp38-cp38-win_amd64.whl", hash = "sha256:e96d7f3096a36c8754207ab89d4b3282ba7b49ea140e4973591852c77d09eb76"},
-    {file = "numpy-1.22.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:4c6036521f11a731ce0648f10c18ae66d7143865f19f7299943c985cdc95afb5"},
-    {file = "numpy-1.22.4-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b89bf9b94b3d624e7bb480344e91f68c1c6c75f026ed6755955117de00917a7c"},
-    {file = "numpy-1.22.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d487e06ecbf1dc2f18e7efce82ded4f705f4bd0cd02677ffccfb39e5c284c7e"},
-    {file = "numpy-1.22.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb268dbd5cfaffd9448113539e44e2dd1c5ca9ce25576f7c04a5453edc26fa"},
-    {file = "numpy-1.22.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37431a77ceb9307c28382c9773da9f306435135fae6b80b62a11c53cfedd8802"},
-    {file = "numpy-1.22.4-cp39-cp39-win32.whl", hash = "sha256:cc7f00008eb7d3f2489fca6f334ec19ca63e31371be28fd5dad955b16ec285bd"},
-    {file = "numpy-1.22.4-cp39-cp39-win_amd64.whl", hash = "sha256:f0725df166cf4785c0bc4cbfb320203182b1ecd30fee6e541c8752a92df6aa32"},
-    {file = "numpy-1.22.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0791fbd1e43bf74b3502133207e378901272f3c156c4df4954cad833b1380207"},
-    {file = "numpy-1.22.4.zip", hash = "sha256:425b390e4619f58d8526b3dcf656dde069133ae5c240229821f01b5f44ea07af"},
-]
+numpy = []
 oauthlib = [
     {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"},
     {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"},
@@ -1724,29 +1624,7 @@ packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
     {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
 ]
-pandas = [
-    {file = "pandas-1.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be67c782c4f1b1f24c2f16a157e12c2693fd510f8df18e3287c77f33d124ed07"},
-    {file = "pandas-1.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5a206afa84ed20e07603f50d22b5f0db3fb556486d8c2462d8bc364831a4b417"},
-    {file = "pandas-1.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0010771bd9223f7afe5f051eb47c4a49534345dfa144f2f5470b27189a4dd3b5"},
-    {file = "pandas-1.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3228198333dd13c90b6434ddf61aa6d57deaca98cf7b654f4ad68a2db84f8cfe"},
-    {file = "pandas-1.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b79af3a69e5175c6fa7b4e046b21a646c8b74e92c6581a9d825687d92071b51"},
-    {file = "pandas-1.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:5586cc95692564b441f4747c47c8a9746792e87b40a4680a2feb7794defb1ce3"},
-    {file = "pandas-1.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:061609334a8182ab500a90fe66d46f6f387de62d3a9cb9aa7e62e3146c712167"},
-    {file = "pandas-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b8134651258bce418cb79c71adeff0a44090c98d955f6953168ba16cc285d9f7"},
-    {file = "pandas-1.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:df82739e00bb6daf4bba4479a40f38c718b598a84654cbd8bb498fd6b0aa8c16"},
-    {file = "pandas-1.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:385c52e85aaa8ea6a4c600a9b2821181a51f8be0aee3af6f2dcb41dafc4fc1d0"},
-    {file = "pandas-1.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:295872bf1a09758aba199992c3ecde455f01caf32266d50abc1a073e828a7b9d"},
-    {file = "pandas-1.4.2-cp38-cp38-win32.whl", hash = "sha256:95c1e422ced0199cf4a34385ff124b69412c4bc912011ce895582bee620dfcaa"},
-    {file = "pandas-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:5c54ea4ef3823108cd4ec7fb27ccba4c3a775e0f83e39c5e17f5094cb17748bc"},
-    {file = "pandas-1.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c072c7f06b9242c855ed8021ff970c0e8f8b10b35e2640c657d2a541c5950f59"},
-    {file = "pandas-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f549097993744ff8c41b5e8f2f0d3cbfaabe89b4ae32c8c08ead6cc535b80139"},
-    {file = "pandas-1.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff08a14ef21d94cdf18eef7c569d66f2e24e0bc89350bcd7d243dd804e3b5eb2"},
-    {file = "pandas-1.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c5bf555b6b0075294b73965adaafb39cf71c312e38c5935c93d78f41c19828a"},
-    {file = "pandas-1.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51649ef604a945f781105a6d2ecf88db7da0f4868ac5d45c51cb66081c4d9c73"},
-    {file = "pandas-1.4.2-cp39-cp39-win32.whl", hash = "sha256:d0d4f13e4be7ce89d7057a786023c461dd9370040bdb5efa0a7fe76b556867a0"},
-    {file = "pandas-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:09d8be7dd9e1c4c98224c4dfe8abd60d145d934e9fc1f5f411266308ae683e6a"},
-    {file = "pandas-1.4.2.tar.gz", hash = "sha256:92bc1fc585f1463ca827b45535957815b7deb218c549b7c18402c322c7549a12"},
-]
+pandas = []
 pathtools = [
     {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"},
 ]
@@ -1859,6 +1737,10 @@ pygments = [
     {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
     {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
 ]
+pyknp = [
+    {file = "pyknp-0.6.1-py3-none-any.whl", hash = "sha256:c988cbd92d645e43d51a406835147a7c24aa849894802e22fcf5c4e1475da805"},
+    {file = "pyknp-0.6.1.tar.gz", hash = "sha256:5fca288a70304c745d20da5657c6175b5eadf7a7a571ed4f57bb157eb6e7df4d"},
+]
 pyparsing = [
     {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
     {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
@@ -1882,10 +1764,7 @@ python-dotenv = [
 python-levenshtein = [
     {file = "python-Levenshtein-0.12.2.tar.gz", hash = "sha256:dc2395fbd148a1ab31090dd113c366695934b9e85fe5a4b2a032745efd0346f6"},
 ]
-pytorch-lightning = [
-    {file = "pytorch-lightning-1.6.4.tar.gz", hash = "sha256:5459f2c3e67676ec59e94576d1499e9559d214e7df41eadd135db64b4ccf54b9"},
-    {file = "pytorch_lightning-1.6.4-py3-none-any.whl", hash = "sha256:0f42f93116a3fcb6fd8c9ea45cf7c918e4aa3f848ae21d0e9ac2bf39f2865dd7"},
-]
+pytorch-lightning = []
 pytz = [
     {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
     {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
@@ -1925,99 +1804,15 @@ pyyaml = [
     {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
     {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
 ]
-regex = [
-    {file = "regex-2022.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:042d122f9fee3ceb6d7e3067d56557df697d1aad4ff5f64ecce4dc13a90a7c01"},
-    {file = "regex-2022.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffef4b30785dc2d1604dfb7cf9fca5dc27cd86d65f7c2a9ec34d6d3ae4565ec2"},
-    {file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0afa6a601acf3c0dc6de4e8d7d8bbce4e82f8542df746226cd35d4a6c15e9456"},
-    {file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a11cbe8eb5fb332ae474895b5ead99392a4ea568bd2a258ab8df883e9c2bf92"},
-    {file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c1f62ee2ba880e221bc950651a1a4b0176083d70a066c83a50ef0cb9b178e12"},
-    {file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aba3d13c77173e9bfed2c2cea7fc319f11c89a36fcec08755e8fb169cf3b0df"},
-    {file = "regex-2022.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249437f7f5b233792234aeeecb14b0aab1566280de42dfc97c26e6f718297d68"},
-    {file = "regex-2022.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:179410c79fa86ef318d58ace233f95b87b05a1db6dc493fa29404a43f4b215e2"},
-    {file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5e201b1232d81ca1a7a22ab2f08e1eccad4e111579fd7f3bbf60b21ef4a16cea"},
-    {file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fdecb225d0f1d50d4b26ac423e0032e76d46a788b83b4e299a520717a47d968c"},
-    {file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:be57f9c7b0b423c66c266a26ad143b2c5514997c05dd32ce7ca95c8b209c2288"},
-    {file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ed657a07d8a47ef447224ea00478f1c7095065dfe70a89e7280e5f50a5725131"},
-    {file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24908aefed23dd065b4a668c0b4ca04d56b7f09d8c8e89636cf6c24e64e67a1e"},
-    {file = "regex-2022.6.2-cp310-cp310-win32.whl", hash = "sha256:775694cd0bb2c4accf2f1cdd007381b33ec8b59842736fe61bdbad45f2ac7427"},
-    {file = "regex-2022.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:809bbbbbcf8258049b031d80932ba71627d2274029386f0452e9950bcfa2c6e8"},
-    {file = "regex-2022.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2b5d983eb0adf2049d41f95205bdc3de4e6cc2350e9c80d4409d3a75229de"},
-    {file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4c101746a8dac0401abefa716b357c546e61ea2e3d4a564a9db9eac57ccbce"},
-    {file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:166ae7674d0a0e0f8044e7335ba86d0716c9d49465cff1b153f908e0470b8300"},
-    {file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5eac5d8a8ac9ccf00805d02a968a36f5c967db6c7d2b747ab9ed782b3b3a28b"},
-    {file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f57823f35b18d82b201c1b27ce4e55f88e79e81d9ca07b50ce625d33823e1439"},
-    {file = "regex-2022.6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d42e3b7b23473729adbf76103e7df75f9167a5a80b1257ca30688352b4bb2dc"},
-    {file = "regex-2022.6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2932e728bee0a634fe55ee54d598054a5a9ffe4cd2be21ba2b4b8e5f8064c2c"},
-    {file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:17764683ea01c2b8f103d99ae9de2473a74340df13ce306c49a721f0b1f0eb9e"},
-    {file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:2ac29b834100d2c171085ceba0d4a1e7046c434ddffc1434dbc7f9d59af1e945"},
-    {file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:f43522fb5d676c99282ca4e2d41e8e2388427c0cf703db6b4a66e49b10b699a8"},
-    {file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:9faa01818dad9111dbf2af26c6e3c45140ccbd1192c3a0981f196255bf7ec5e6"},
-    {file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:17443f99b8f255273731f915fdbfea4d78d809bb9c3aaf67b889039825d06515"},
-    {file = "regex-2022.6.2-cp36-cp36m-win32.whl", hash = "sha256:4a5449adef907919d4ce7a1eab2e27d0211d1b255bf0b8f5dd330ad8707e0fc3"},
-    {file = "regex-2022.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4d206703a96a39763b5b45cf42645776f5553768ea7f3c2c1a39a4f59cafd4ba"},
-    {file = "regex-2022.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fcd7c432202bcb8b642c3f43d5bcafc5930d82fe5b2bf2c008162df258445c1d"},
-    {file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:186c5a4a4c40621f64d771038ede20fca6c61a9faa8178f9e305aaa0c2442a97"},
-    {file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:047b2d1323a51190c01b6604f49fe09682a5c85d3c1b2c8b67c1cd68419ce3c4"},
-    {file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30637e7fa4acfed444525b1ab9683f714be617862820578c9fd4e944d4d9ad1f"},
-    {file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adafe6f2c6d86dbf3313866b61180530ca4dcd0c264932dc8fa1ffb10871d58"},
-    {file = "regex-2022.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67ae3601edf86e15ebe40885e5bfdd6002d34879070be15cf18fc0d80ea24fed"},
-    {file = "regex-2022.6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:48dddddce0ea7e7c3e92c1e0c5a28c13ca4dc9cf7e996c706d00479652bff76c"},
-    {file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:68e5c641645351eb9eb12c465876e76b53717f99e9b92aea7a2dd645a87aa7aa"},
-    {file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8fd5f8ae42f789538bb634bdfd69b9aa357e76fdfd7ad720f32f8994c0d84f1e"},
-    {file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:71988a76fcb68cc091e901fddbcac0f9ad9a475da222c47d3cf8db0876cb5344"},
-    {file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:4b8838f70be3ce9e706df9d72f88a0aa7d4c1fea61488e06fdf292ccb70ad2be"},
-    {file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:663dca677bd3d2e2b5b7d0329e9f24247e6f38f3b740dd9a778a8ef41a76af41"},
-    {file = "regex-2022.6.2-cp37-cp37m-win32.whl", hash = "sha256:24963f0b13cc63db336d8da2a533986419890d128c551baacd934c249d51a779"},
-    {file = "regex-2022.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ceff75127f828dfe7ceb17b94113ec2df4df274c4cd5533bb299cb099a18a8ca"},
-    {file = "regex-2022.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a6f2698cfa8340dfe4c0597782776b393ba2274fe4c079900c7c74f68752705"},
-    {file = "regex-2022.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8a08ace913c4101f0dc0be605c108a3761842efd5f41a3005565ee5d169fb2b"},
-    {file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26dbe90b724efef7820c3cf4a0e5be7f130149f3d2762782e4e8ac2aea284a0b"},
-    {file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5f759a1726b995dc896e86f17f9c0582b54eb4ead00ed5ef0b5b22260eaf2d0"},
-    {file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fc26bb3415e7aa7495c000a2c13bf08ce037775db98c1a3fac9ff04478b6930"},
-    {file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52684da32d9003367dc1a1c07e059b9bbaf135ad0764cd47d8ac3dba2df109bc"},
-    {file = "regex-2022.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c1264eb40a71cf2bff43d6694ab7254438ca19ef330175060262b3c8dd3931a"},
-    {file = "regex-2022.6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc635ab319c9b515236bdf327530acda99be995f9d3b9f148ab1f60b2431e970"},
-    {file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:27624b490b5d8880f25dac67e1e2ea93dfef5300b98c6755f585799230d6c746"},
-    {file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:555f7596fd1f123f8c3a67974c01d6ef80b9769e04d660d6c1a7cc3e6cff7069"},
-    {file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:933e72fbe1829cbd59da2bc51ccd73d73162f087f88521a87a8ec9cb0cf10fa8"},
-    {file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:cff5c87e941292c97d11dc81bd20679f56a2830f0f0e32f75b8ed6e0eb40f704"},
-    {file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c757f3a27b6345de13ef3ca956aa805d7734ce68023e84d0fc74e1f09ce66f7a"},
-    {file = "regex-2022.6.2-cp38-cp38-win32.whl", hash = "sha256:a58d21dd1a2d6b50ed091554ff85e448fce3fe33a4db8b55d0eba2ca957ed626"},
-    {file = "regex-2022.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:495a4165172848503303ed05c9d0409428f789acc27050fe2cf0a4549188a7d5"},
-    {file = "regex-2022.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1ab5cf7d09515548044e69d3a0ec77c63d7b9dfff4afc19653f638b992573126"},
-    {file = "regex-2022.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1ea28f0ee6cbe4c0367c939b015d915aa9875f6e061ba1cf0796ca9a3010570"},
-    {file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3de1ecf26ce85521bf73897828b6d0687cc6cf271fb6ff32ac63d26b21f5e764"},
-    {file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7c7044aabdad2329974be2246babcc21d3ede852b3971a90fd8c2056c20360"},
-    {file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53d69d77e9cfe468b000314dd656be85bb9e96de088a64f75fe128dfe1bf30dd"},
-    {file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c8d61883a38b1289fba9944a19a361875b5c0170b83cdcc95ea180247c1b7d3"},
-    {file = "regex-2022.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5429202bef174a3760690d912e3a80060b323199a61cef6c6c29b30ce09fd17"},
-    {file = "regex-2022.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e85b10280cf1e334a7c95629f6cbbfe30b815a4ea5f1e28d31f79eb92c2c3d93"},
-    {file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c400dfed4137f32127ea4063447006d7153c974c680bf0fb1b724cce9f8567fc"},
-    {file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f648037c503985aed39f85088acab6f1eb6a0482d7c6c665a5712c9ad9eaefc"},
-    {file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e7b2ff451f6c305b516281ec45425dd423223c8063218c5310d6f72a0a7a517c"},
-    {file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:be456b4313a86be41706319c397c09d9fdd2e5cdfde208292a277b867e99e3d1"},
-    {file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c3db393b21b53d7e1d3f881b64c29d886cbfdd3df007e31de68b329edbab7d02"},
-    {file = "regex-2022.6.2-cp39-cp39-win32.whl", hash = "sha256:d70596f20a03cb5f935d6e4aad9170a490d88fc4633679bf00c652e9def4619e"},
-    {file = "regex-2022.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:3b9b6289e03dbe6a6096880d8ac166cb23c38b4896ad235edee789d4e8697152"},
-    {file = "regex-2022.6.2.tar.gz", hash = "sha256:f7b43acb2c46fb2cd506965b2d9cf4c5e64c9c612bac26c1187933c7296bf08c"},
-]
-requests = [
-    {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
-    {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
-]
+regex = []
+requests = []
 requests-oauthlib = [
     {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
     {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"},
 ]
 rhoknp = []
-rich = [
-    {file = "rich-12.4.4-py3-none-any.whl", hash = "sha256:d2bbd99c320a2532ac71ff6a3164867884357da3e3301f0240090c5d2fdac7ec"},
-    {file = "rich-12.4.4.tar.gz", hash = "sha256:4c586de507202505346f3e32d1363eb9ed6932f0c2f63184dea88983ff4971e2"},
-]
-rsa = [
-    {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"},
-    {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"},
-]
+rich = []
+rsa = []
 scikit-learn = [
     {file = "scikit-learn-1.1.1.tar.gz", hash = "sha256:3e77b71e8e644f86c8b5be7f1c285ef597de4c384961389ee3e9ca36c445b256"},
     {file = "scikit_learn-1.1.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:102f51797cd8944bf44a038d106848ddf2804f2c1edf7aea45fba81a4fdc4d80"},
@@ -2107,10 +1902,7 @@ sentencepiece = [
     {file = "sentencepiece-0.1.96-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48c6d13b3bfff08060c138248e85df60f6fad11135ad7a8fc2ef6005aacca839"},
     {file = "sentencepiece-0.1.96.tar.gz", hash = "sha256:9bdf097d5bd1d8ce42dfee51f6ff05f5578b96e48c6f6006aa4eff69edfa3639"},
 ]
-sentry-sdk = [
-    {file = "sentry-sdk-1.5.12.tar.gz", hash = "sha256:259535ba66933eacf85ab46524188c84dcb4c39f40348455ce15e2c0aca68863"},
-    {file = "sentry_sdk-1.5.12-py2.py3-none-any.whl", hash = "sha256:778b53f0a6c83b1ee43d3b7886318ba86d975e686cb2c7906ccc35b334360be1"},
-]
+sentry-sdk = []
 seqeval = [
     {file = "seqeval-1.2.2.tar.gz", hash = "sha256:f28e97c3ab96d6fcd32b648f6438ff2e09cfba87f05939da9b3970713ec56e6f"},
 ]
@@ -2274,34 +2066,19 @@ torch = [
     {file = "torch-1.11.0-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:50fd9bf85c578c871c28f1cb0ace9dfc6024401c7f399b174fb0f370899f4454"},
     {file = "torch-1.11.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:0e48af66ad755f0f9c5f2664028a414f57c49d6adc37e77e06fe0004da4edb61"},
 ]
-torchmetrics = [
-    {file = "torchmetrics-0.9.0-py3-none-any.whl", hash = "sha256:4fec7d3d070dafba9dc024853bd7c654fd3f7d2c2fe72fab9415cafccd719b8e"},
-    {file = "torchmetrics-0.9.0.tar.gz", hash = "sha256:3aa32ea575915b313d872d3460996c0f12a7bb37e6ce3da0e8d80865603b89f6"},
-]
+torchmetrics = []
 tqdm = [
     {file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"},
     {file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"},
 ]
 transformers = [
-    {file = "transformers-4.19.2-py3-none-any.whl", hash = "sha256:1416315b7c5ff1f56d3915f416b67aa254a9907fbb73ef7f7bffc9210446b5fa"},
-    {file = "transformers-4.19.2.tar.gz", hash = "sha256:e19a4ff07458eda143c738e5259caf48449fcf078a63d6b1bd1aa806543440a3"},
-]
-typer = [
-    {file = "typer-0.4.1-py3-none-any.whl", hash = "sha256:e8467f0ebac0c81366c2168d6ad9f888efdfb6d4e1d3d5b4a004f46fa444b5c3"},
-    {file = "typer-0.4.1.tar.gz", hash = "sha256:5646aef0d936b2c761a10393f0384ee6b5c7fe0bb3e5cd710b17134ca1d99cff"},
-]
-typing-extensions = [
-    {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
-    {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
-]
-urllib3 = [
-    {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
-    {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
-]
-wandb = [
-    {file = "wandb-0.12.17-py2.py3-none-any.whl", hash = "sha256:40e599ed7a4a633a4e1da77d026ee872fcb60a207aafbd1bf8ec1ab5b8171ccf"},
-    {file = "wandb-0.12.17.tar.gz", hash = "sha256:ad2fe5a9cbb44c445cb0cfc6d04804f74dca2999ae98f0c8db93721b522f76f1"},
+    {file = "transformers-4.19.4-py3-none-any.whl", hash = "sha256:572d8ecbff29ec53769e0459b4334ebd1038f75ad25119a3006f8816643dccc4"},
+    {file = "transformers-4.19.4.tar.gz", hash = "sha256:b8c0f9816b4c4c2f0265b24d0a0b9d4ae8b7b98fc779d63f92e3c121c4c2d483"},
 ]
+typer = []
+typing-extensions = []
+urllib3 = []
+wandb = []
 werkzeug = [
     {file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"},
     {file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"},
@@ -2380,7 +2157,4 @@ yarl = [
     {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
     {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
 ]
-zipp = [
-    {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
-    {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
-]
+zipp = []
diff --git a/pyproject.toml b/pyproject.toml
index 47d0086d..61fc37b0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,7 @@ ipadic = "^1.0.0"
 python-dotenv = "^0.20.0"
 pandas = "^1.4.2"
 dartsclone = "^0.10.2"
+kyoto-reader = "^2.5.2"
 
 [tool.poetry.dev-dependencies]
 pytest = "^7.1"
diff --git a/score.csv b/score.csv
new file mode 100644
index 00000000..113ba65c
--- /dev/null
+++ b/score.csv
@@ -0,0 +1,4 @@
+case,overt,dep,zero_endophora,zero_exophora,zero,dep_zero,all,bridging,coreference
+ガ,0.0,1.0,1.0,0.0,1.0,1.0,1.0
+ヲ,0.0,0.0,1.0,0.0,1.0,1.0,0.666667
+all_case,0.0,1.0,1.0,0.0,1.0,1.0,0.857143,1.0,0.0
diff --git a/scripts/add_features.py b/scripts/add_features.py
new file mode 100644
index 00000000..1ee211b2
--- /dev/null
+++ b/scripts/add_features.py
@@ -0,0 +1,279 @@
+import multiprocessing as mp
+import re
+import textwrap
+from argparse import ArgumentParser
+from pathlib import Path
+from subprocess import PIPE, Popen
+
+from kyoto_reader import KyotoReader
+from rhoknp import KNP, Document, Jumanpp, Sentence
+from tqdm import tqdm
+
+FEATURES_PATTERN = re.compile(r"(?P<features>(<[^>]+>)+)")
+
+
+class JumanppAugmenter:
+    def __init__(self):
+        self.jumanpp = Jumanpp(options=["--partial-input"])
+
+    def augment_document(self, original_document: Document, update_original: bool = True) -> Document:
+        buf = ""
+        for sentence in original_document.sentences:
+            buf += self._create_partial_input(sentence)
+
+        with Popen(self.jumanpp.run_command, stdout=PIPE, stdin=PIPE, encoding="utf-8") as p:
+            jumanpp_text, _ = p.communicate(input=buf)
+        augmented_document = Document.from_jumanpp(jumanpp_text)
+
+        for original_sentence, augmented_sentence in zip(original_document.sentences, augmented_document.sentences):
+            self._postprocess_sentence(original_sentence, augmented_sentence, update_original=update_original)
+        return augmented_document
+
+    def augment_sentence(self, original_sentence: Sentence, update_original: bool = True) -> Sentence:
+        buf = self._create_partial_input(original_sentence)
+        with Popen(self.jumanpp.run_command, stdout=PIPE, stdin=PIPE, encoding="utf-8") as p:
+            jumanpp_text, _ = p.communicate(input=buf)
+        augmented_sentence = Sentence.from_jumanpp(jumanpp_text)
+
+        self._postprocess_sentence(original_sentence, augmented_sentence, update_original=update_original)
+        return augmented_sentence
+
+    @staticmethod
+    def _create_partial_input(sentence: Sentence) -> str:
+        """
+        create raw string for jumanpp --partial-input
+        """
+        buf = ""
+        for morpheme in sentence.morphemes:
+            buf += (
+                f"\t{morpheme.surf}"
+                f"\treading:{morpheme.reading}"
+                f"\tbaseform:{morpheme.lemma}"
+                f"\tpos:{morpheme.pos}"
+                f"\tsubpos:{morpheme.subpos}"
+                f"\tconjtype:{morpheme.conjtype}"
+                f"\tconjform:{morpheme.conjform}\n"
+            )
+        buf += "\n"
+        return buf
+
+    @staticmethod
+    def _postprocess_sentence(
+        original_sentence: Sentence, augmented_sentence: Sentence, update_original: bool = True
+    ) -> None:
+        for original_morpheme, augmented_morpheme in zip(original_sentence.morphemes, augmented_sentence.morphemes):
+            # Jumanpp may override reading
+            augmented_morpheme._attributes.reading = original_morpheme.reading
+            if update_original and not original_sentence.need_knp:
+                # convert Semantics to Features
+                for k, v in augmented_morpheme.semantics.items():
+                    original_morpheme.features[k] = v
+
+
+def add_features(doc_ids: list[str], reader: KyotoReader, output_dir: Path) -> None:
+    knp = KNP(options=["-tab", "-dpnd-fast", "-read-feature"])
+    jumanpp_augmenter = JumanppAugmenter()
+
+    for doc_id in tqdm(doc_ids):
+        old_knp_lines = reader.get_knp(doc_id).split("\n")
+        buf = []
+        for idx, line in enumerate(old_knp_lines):
+            if line.startswith("*") or line.startswith("+"):
+                pass
+            elif mo := FEATURES_PATTERN.search(line):
+                features = mo.group("features")
+                old_knp_lines[idx] = line.replace(features, "")
+                buf.append((idx, features))
+
+        # 素性付与
+        with Popen(knp.run_command, stdout=PIPE, stdin=PIPE, encoding="utf-8", errors="replace") as p:
+            knp_text, _ = p.communicate(input="\n".join(old_knp_lines))
+
+        new_knp_lines = knp_text.split("\n")
+        # ダ列文語連体形など
+        if len(new_knp_lines) != len(old_knp_lines):
+            continue
+
+        for idx, features in buf:
+            assert new_knp_lines[idx].endswith(">")
+            new_knp_lines[idx] += features
+        knp_text = "\n".join(new_knp_lines)
+        document = Document.from_knp(knp_text)
+
+        # 形態素意味情報付与 (引数に渡したdocumentをupdateする)
+        _ = jumanpp_augmenter.augment_document(document)
+
+        with output_dir.joinpath(f"{document.doc_id}.knp").open(mode="w") as f:
+            f.write(document.to_knp())
+
+
+def test_jumanpp_augmenter():
+    jumanpp_augmenter = JumanppAugmenter()
+
+    knp_text = textwrap.dedent(
+        """\
+        # S-ID:w201106-0000060050-1 JUMAN:6.1-20101108 KNP:3.1-20101107 DATE:2011/06/21 SCORE:-44.94406 MOD:2017/10/15 MEMO:
+        * 2D
+        + 1D
+        コイン こいん コイン 名詞 6 普通名詞 1 * 0 * 0
+        + 3D <rel type="ガ" target="不特定:人"/><rel type="ヲ" target="コイン" sid="w201106-0000060050-1" id="0"/>
+        トス とす トス 名詞 6 サ変名詞 2 * 0 * 0
+        を を を 助詞 9 格助詞 1 * 0 * 0
+        * 2D
+        + 3D
+        ３ さん ３ 名詞 6 数詞 7 * 0 * 0
+        回 かい 回 接尾辞 14 名詞性名詞助数辞 3 * 0 * 0
+        * -1D
+        + -1D <rel type="ガ" target="不特定:人"/><rel type="ガ" mode="？" target="読者"/><rel type="ガ" mode="？" target="著者"/><rel type="ヲ" target="トス" sid="w201106-0000060050-1" id="1"/>
+        行う おこなう 行う 動詞 2 * 0 子音動詞ワ行 12 基本形 2
+        。 。 。 特殊 1 句点 1 * 0 * 0
+        EOS
+        """
+    )
+    sentence = Sentence.from_knp(knp_text)
+    _ = jumanpp_augmenter.augment_sentence(sentence)
+    expected = textwrap.dedent(
+        """\
+        # S-ID:w201106-0000060050-1 JUMAN:6.1-20101108 KNP:3.1-20101107 DATE:2011/06/21 SCORE:-44.94406 MOD:2017/10/15 MEMO:
+        * 2D
+        + 1D
+        コイン こいん コイン 名詞 6 普通名詞 1 * 0 * 0 <自動獲得:Wikipedia><Wikipediaリダイレクト:硬貨>
+        + 3D <rel type="ガ" target="不特定:人"/><rel type="ヲ" target="コイン" sid="w201106-0000060050-1" id="0"/>
+        トス とす トス 名詞 6 サ変名詞 2 * 0 * 0 <代表表記:トス/とす><ドメイン:スポーツ><カテゴリ:抽象物>
+        を を を 助詞 9 格助詞 1 * 0 * 0
+        * 2D
+        + 3D
+        ３ さん ３ 名詞 6 数詞 7 * 0 * 0 <カテゴリ:数量>
+        回 かい 回 接尾辞 14 名詞性名詞助数辞 3 * 0 * 0 <代表表記:回/かい><準内容語>
+        * -1D
+        + -1D <rel type="ガ" target="不特定:人"/><rel type="ガ" mode="？" target="読者"/><rel type="ガ" mode="？" target="著者"/><rel type="ヲ" target="トス" sid="w201106-0000060050-1" id="1"/>
+        行う おこなう 行う 動詞 2 * 0 子音動詞ワ行 12 基本形 2 <代表表記:行う/おこなう>
+        。 。 。 特殊 1 句点 1 * 0 * 0
+        EOS
+        """
+    )
+    assert sentence.to_knp() == expected
+
+    knp_text = textwrap.dedent(
+        """\
+        # S-ID:w201106-0000060050-1 JUMAN:6.1-20101108 KNP:3.1-20101107 DATE:2011/06/21 SCORE:-44.94406 MOD:2017/10/15 MEMO:
+        * 2D
+        + 1D
+        コイン こいん コイン 名詞 6 普通名詞 1 * 0 * 0
+        + 3D <rel type="ガ" target="不特定:人"/><rel type="ヲ" target="コイン" sid="w201106-0000060050-1" id="0"/>
+        トス とす トス 名詞 6 サ変名詞 2 * 0 * 0
+        を を を 助詞 9 格助詞 1 * 0 * 0
+        * 2D
+        + 3D
+        ３ さん ３ 名詞 6 数詞 7 * 0 * 0
+        回 かい 回 接尾辞 14 名詞性名詞助数辞 3 * 0 * 0
+        * -1D
+        + -1D <rel type="ガ" target="不特定:人"/><rel type="ガ" mode="？" target="読者"/><rel type="ガ" mode="？" target="著者"/><rel type="ヲ" target="トス" sid="w201106-0000060050-1" id="1"/>
+        行う おこなう 行う 動詞 2 * 0 子音動詞ワ行 12 基本形 2
+        。 。 。 特殊 1 句点 1 * 0 * 0
+        EOS
+        # S-ID:w201106-0000060050-2 JUMAN:6.1-20101108 KNP:3.1-20101107 DATE:2011/06/21 SCORE:-64.95916 MOD:2013/04/13
+        * 1D
+        + 1D <rel type="ノ" target="コイン" sid="w201106-0000060050-1" id="0"/>
+        表 おもて 表 名詞 6 普通名詞 1 * 0 * 0
+        が が が 助詞 9 格助詞 1 * 0 * 0
+        * 2D
+        + 2D <rel type="ガ" target="表" sid="w201106-0000060050-2" id="0"/><rel type="外の関係" target="数" sid="w201106-0000060050-2" id="2"/>
+        出た でた 出る 動詞 2 * 0 母音動詞 1 タ形 10
+        * 5D
+        + 5D <rel type="ノ" target="出た" sid="w201106-0000060050-2" id="1"/>
+        数 かず 数 名詞 6 普通名詞 1 * 0 * 0
+        だけ だけ だけ 助詞 9 副助詞 2 * 0 * 0
+        、 、 、 特殊 1 読点 2 * 0 * 0
+        * 4D
+        + 4D
+        フィールド ふぃーるど フィールド 名詞 6 普通名詞 1 * 0 * 0
+        上 じょう 上 接尾辞 14 名詞性名詞接尾辞 2 * 0 * 0
+        の の の 助詞 9 接続助詞 3 * 0 * 0
+        * 5D
+        + 5D <rel type="修飾" target="フィールド上" sid="w201106-0000060050-2" id="3"/><rel type="修飾" mode="AND" target="数" sid="w201106-0000060050-2" id="2"/>
+        モンスター もんすたー モンスター 名詞 6 普通名詞 1 * 0 * 0
+        を を を 助詞 9 格助詞 1 * 0 * 0
+        * -1D
+        + -1D <rel type="ヲ" target="モンスター" sid="w201106-0000060050-2" id="4"/><rel type="ガ" target="不特定:状況"/>
+        破壊 はかい 破壊 名詞 6 サ変名詞 2 * 0 * 0
+        する する する 動詞 2 * 0 サ変動詞 16 基本形 2
+        。 。 。 特殊 1 句点 1 * 0 * 0
+        EOS
+        """
+    )
+    document = Document.from_knp(knp_text)
+    _ = jumanpp_augmenter.augment_document(document)
+    expected = textwrap.dedent(
+        """\
+        # S-ID:w201106-0000060050-1 JUMAN:6.1-20101108 KNP:3.1-20101107 DATE:2011/06/21 SCORE:-44.94406 MOD:2017/10/15 MEMO:
+        * 2D
+        + 1D
+        コイン こいん コイン 名詞 6 普通名詞 1 * 0 * 0 <自動獲得:Wikipedia><Wikipediaリダイレクト:硬貨>
+        + 3D <rel type="ガ" target="不特定:人"/><rel type="ヲ" target="コイン" sid="w201106-0000060050-1" id="0"/>
+        トス とす トス 名詞 6 サ変名詞 2 * 0 * 0 <代表表記:トス/とす><ドメイン:スポーツ><カテゴリ:抽象物>
+        を を を 助詞 9 格助詞 1 * 0 * 0
+        * 2D
+        + 3D
+        ３ さん ３ 名詞 6 数詞 7 * 0 * 0 <カテゴリ:数量>
+        回 かい 回 接尾辞 14 名詞性名詞助数辞 3 * 0 * 0 <代表表記:回/かい><準内容語>
+        * -1D
+        + -1D <rel type="ガ" target="不特定:人"/><rel type="ガ" mode="？" target="読者"/><rel type="ガ" mode="？" target="著者"/><rel type="ヲ" target="トス" sid="w201106-0000060050-1" id="1"/>
+        行う おこなう 行う 動詞 2 * 0 子音動詞ワ行 12 基本形 2 <代表表記:行う/おこなう>
+        。 。 。 特殊 1 句点 1 * 0 * 0
+        EOS
+        # S-ID:w201106-0000060050-2 JUMAN:6.1-20101108 KNP:3.1-20101107 DATE:2011/06/21 SCORE:-64.95916 MOD:2013/04/13
+        * 1D
+        + 1D <rel type="ノ" target="コイン" sid="w201106-0000060050-1" id="0"/>
+        表 おもて 表 名詞 6 普通名詞 1 * 0 * 0 <代表表記:表/おもて><カテゴリ:場所-機能><漢字読み:訓>
+        が が が 助詞 9 格助詞 1 * 0 * 0
+        * 2D
+        + 2D <rel type="ガ" target="表" sid="w201106-0000060050-2" id="0"/><rel type="外の関係" target="数" sid="w201106-0000060050-2" id="2"/>
+        出た でた 出る 動詞 2 * 0 母音動詞 1 タ形 10 <代表表記:出る/でる><反義:動詞:入る/はいる><自他動詞:他:出す/だす><補文ト>
+        * 5D
+        + 5D <rel type="ノ" target="出た" sid="w201106-0000060050-2" id="1"/>
+        数 かず 数 名詞 6 普通名詞 1 * 0 * 0 <代表表記:数/かず><カテゴリ:数量><漢字読み:訓>
+        だけ だけ だけ 助詞 9 副助詞 2 * 0 * 0
+        、 、 、 特殊 1 読点 2 * 0 * 0
+        * 4D
+        + 4D
+        フィールド ふぃーるど フィールド 名詞 6 普通名詞 1 * 0 * 0 <代表表記:フィールド/ふぃーるど><カテゴリ:場所-その他>
+        上 じょう 上 接尾辞 14 名詞性名詞接尾辞 2 * 0 * 0 <代表表記:上/じょう>
+        の の の 助詞 9 接続助詞 3 * 0 * 0
+        * 5D
+        + 5D <rel type="修飾" target="フィールド上" sid="w201106-0000060050-2" id="3"/><rel type="修飾" mode="AND" target="数" sid="w201106-0000060050-2" id="2"/>
+        モンスター もんすたー モンスター 名詞 6 普通名詞 1 * 0 * 0 <代表表記:モンスター/もんすたー><カテゴリ:人>
+        を を を 助詞 9 格助詞 1 * 0 * 0
+        * -1D
+        + -1D <rel type="ヲ" target="モンスター" sid="w201106-0000060050-2" id="4"/><rel type="ガ" target="不特定:状況"/>
+        破壊 はかい 破壊 名詞 6 サ変名詞 2 * 0 * 0 <代表表記:破壊/はかい><カテゴリ:抽象物><反義:名詞-サ変名詞:建設/けんせつ>
+        する する する 動詞 2 * 0 サ変動詞 16 基本形 2 <代表表記:する/する><自他動詞:自:成る/なる><付属動詞候補（基本）>
+        。 。 。 特殊 1 句点 1 * 0 * 0
+        EOS
+        """
+    )
+    assert document.to_knp() == expected
+
+
+def main():
+    parser = ArgumentParser()
+    parser.add_argument("INPUT", type=str, help="path to input knp dir")
+    parser.add_argument("OUTPUT", type=str, help="path to output dir")
+    parser.add_argument("-j", default=1, type=int, help="number of jobs")
+    args = parser.parse_args()
+
+    reader = KyotoReader(args.INPUT, did_from_sid=True)
+    output_dir = Path(args.OUTPUT)
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    doc_ids = reader.doc_ids
+    chunk_size = len(doc_ids) // args.j + int(len(doc_ids) % args.j > 0)
+    iterable = [
+        (doc_ids[slice(start, start + chunk_size)], reader, output_dir) for start in range(0, len(doc_ids), chunk_size)
+    ]
+    with mp.Pool(args.j) as pool:
+        pool.starmap(add_features, iterable)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/build_datasets.sh b/scripts/build_datasets.sh
new file mode 100755
index 00000000..3546874d
--- /dev/null
+++ b/scripts/build_datasets.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/zsh
+
+while getopts a:w:s:j:o: OPT
+do
+  case $OPT in
+    a) ACTIVATOR="$OPTARG" ;;
+    w) WORK_DIR="$OPTARG" ;;
+    s) SCRIPTS="$OPTARG" ;;
+    j) JOBS="$OPTARG" ;;
+    o) OUT_DIR="$OPTARG" ;;
+    *) echo "invalid option";;
+  esac
+done
+
+# shellcheck source=/dev/null
+source "$ACTIVATOR"
+mkdir -p "$WORK_DIR" "$OUT_DIR"/{kwdlc,fuman}
+cd "$WORK_DIR" || return
+git clone git@github.com:ku-nlp/KyotoCorpusFull.git
+git clone git@github.com:ku-nlp/KWDLC.git
+git clone git@github.com:ku-nlp/AnnotatedFKCCorpus.git
+python "$SCRIPTS"/add_features.py KyotoCorpusFull/knp kyoto/knp -j "$JOBS"
+python "$SCRIPTS"/add_features.py KWDLC/knp kwdlc/knp -j "$JOBS"
+python "$SCRIPTS"/add_features.py AnnotatedFKCCorpus/knp fuman/knp -j "$JOBS"
+mkdir -p kyoto/split
+kyoto idsplit \
+  --corpus-dir kyoto/knp \
+  --output-dir kyoto/split \
+  --train KyotoCorpusFull/id/full/train.id \
+  --valid KyotoCorpusFull/id/full/dev.id \
+  --test KyotoCorpusFull/id/full/test.id
+kyoto idsplit \
+  --corpus-dir kwdlc/knp \
+  --output-dir "$OUT_DIR"/kwdlc \
+  --train KWDLC/id/split_for_pas/train.id \
+  --valid KWDLC/id/split_for_pas/dev.id \
+  --test KWDLC/id/split_for_pas/test.id
+kyoto idsplit \
+  --corpus-dir fuman/knp \
+  --output-dir "$OUT_DIR"/fuman \
+  --train AnnotatedFKCCorpus/id/split_for_pas/train.id \
+  --valid AnnotatedFKCCorpus/id/split_for_pas/dev.id \
+  --test AnnotatedFKCCorpus/id/split_for_pas/test.id
+python "$SCRIPTS"/split_document.py kyoto/split "$OUT_DIR"/kyoto
diff --git a/scripts/char_analyze.py b/scripts/char_analyze.py
index a2cc7367..d79d2ccc 100644
--- a/scripts/char_analyze.py
+++ b/scripts/char_analyze.py
@@ -1,19 +1,17 @@
-from typing import Union
+import sys
 
 import hydra
 import pytorch_lightning as pl
-import transformers.utils.logging as hf_logging
 from dotenv import load_dotenv
 from omegaconf import DictConfig
 from pytorch_lightning.callbacks import Callback
 from torch.utils.data import DataLoader
 
+from jula.cli.utils import suppress_debug_info
 from jula.datamodule.datasets.raw_text_dataset import RawTextDataset
 from jula.models.char_module import CharModule
-from jula.models.typo_module import TypoModule
-from jula.models.word_module import WordModule
 
-hf_logging.set_verbosity(hf_logging.ERROR)
+suppress_debug_info()
 
 
 @hydra.main(version_base=None, config_path="../configs", config_name="word_segmenter")
@@ -33,26 +31,19 @@ def main(cfg: DictConfig):
     trainer: pl.Trainer = hydra.utils.instantiate(
         cfg.trainer,
         logger=None,
+        enable_progress_bar=False,
         callbacks=callbacks,
         devices=cfg.devices,
     )
 
-    model: Union[TypoModule, CharModule, WordModule]
-    # add config name if you want
-    if cfg.config_name in cfg.module.typo:
-        model = TypoModule.load_from_checkpoint(checkpoint_path=cfg.checkpoint_path, hparams=cfg)
-    elif cfg.config_name in cfg.module.char:
+    if cfg.config_name in cfg.module.char:
         model = CharModule.load_from_checkpoint(checkpoint_path=cfg.checkpoint_path, hparams=cfg)
-    elif cfg.config_name in cfg.module.word:
-        model = WordModule.load_from_checkpoint(checkpoint_path=cfg.checkpoint_path, hparams=cfg)
     else:
         raise ValueError("invalid config name")
 
-    inp = input()
-
     # TODO: Use hydra for configuration
     dataset = RawTextDataset(
-        inp.split("\n"),
+        sys.stdin.readlines(),
         model_name_or_path=cfg.datamodule.model_name_or_path,
         max_seq_length=cfg.datamodule.max_seq_length,
         tokenizer_kwargs=cfg.datamodule.tokenizer_kwargs,
diff --git a/scripts/split_document.py b/scripts/split_document.py
new file mode 100644
index 00000000..cb63587d
--- /dev/null
+++ b/scripts/split_document.py
@@ -0,0 +1,69 @@
+from argparse import ArgumentParser
+from pathlib import Path
+from typing import Union
+
+from rhoknp import Document
+from tqdm import tqdm
+from transformers import AutoTokenizer, PreTrainedTokenizer, PreTrainedTokenizerFast
+
+
+# c.f. https://github.com/nobu-g/cohesion-analysis/blob/main/src/preprocess.py
+def split_document(
+    input_dir: Path,
+    output_dir: Path,
+    max_token_length: int,
+    tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast],
+):
+    for knp_file in tqdm(input_dir.glob("**/*.knp")):
+        split = knp_file.parent.name
+        with knp_file.open(mode="r") as f:
+            document = Document.from_knp(f.read())
+
+        cum_lens = [0]
+        for sentence in document.sentences:
+            num_tokens = len(tokenizer.tokenize(" ".join(morpheme.surf for morpheme in sentence.morphemes)))
+            cum_lens.append(cum_lens[-1] + num_tokens)
+
+        end = 1
+        # end を探索
+        while end < len(document.sentences) and cum_lens[end + 1] - cum_lens[0] <= max_token_length:
+            end += 1
+
+        idx = 0
+        while end < len(document.sentences) + 1:
+            start = 0
+            # start を探索
+            while cum_lens[end] - cum_lens[start] > max_token_length:
+                start += 1
+                if start == end - 1:
+                    break
+
+            basename = f"{document.doc_id}-{idx:02}.knp"
+            output_dir.joinpath(split).mkdir(parents=True, exist_ok=True)
+            with output_dir.joinpath(split).joinpath(basename).open(mode="w") as f:
+                f.write("".join(sentence.to_knp() for sentence in document.sentences[start:end]))  # start から end まで書き出し
+            idx += 1
+            end += 1
+
+
+def main():
+    parser = ArgumentParser()
+    parser.add_argument("INPUT", help="path to input dir")
+    parser.add_argument("OUTPUT", help="path to output dir")
+    parser.add_argument(
+        "--max-token-length", default=500, type=int, help="truncate document up to the max token length"
+    )
+    parser.add_argument(
+        "--tokenizer",
+        default="/mnt/zamia/kodama/jula/model/roberta-base-japanese",
+        type=str,
+        help="pretrained model name or path",
+    )
+    args = parser.parse_args()
+
+    tokenizer = AutoTokenizer.from_pretrained(args.tokenizer)
+    split_document(Path(args.INPUT), Path(args.OUTPUT), args.max_token_length, tokenizer)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/split_knp_by_did.py b/scripts/split_knp_by_did.py
new file mode 100644
index 00000000..debdbba2
--- /dev/null
+++ b/scripts/split_knp_by_did.py
@@ -0,0 +1,30 @@
+import argparse
+import logging
+import pathlib
+
+from kyoto_reader import KyotoReader
+from tqdm import tqdm
+
+logger = logging.getLogger(__name__)
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Split a KNP file according to document IDs")
+    parser.add_argument("--input_file", help="Input file")
+    parser.add_argument("--output_dir", help="Output directory")
+    parser.add_argument("--ext", default=".knp", help="Output file extension")
+    args = parser.parse_args()
+
+    input_file = pathlib.Path(args.input_file)
+    reader = KyotoReader(input_file, did_from_sid=True)
+    output_dir = pathlib.Path(args.output_dir)
+
+    for doc_id in tqdm(reader.doc_ids):
+        knp = reader.get_knp(doc_id)
+        output_dir.mkdir(parents=True, exist_ok=True)
+        with output_dir.joinpath(f"{doc_id}{args.ext}").open(mode="wt") as f:
+            f.write(knp)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/train.py b/scripts/train.py
index af6f5bb2..0f36fb82 100644
--- a/scripts/train.py
+++ b/scripts/train.py
@@ -50,7 +50,7 @@ def main(cfg: DictConfig):
     elif cfg.config_name in cfg.module.word:
         model = WordModule(hparams=cfg)
     else:
-        raise ValueError("invalid config name")
+        raise ValueError(f"invalid config name: `{cfg.config_name}`")
 
     trainer.fit(model=model, datamodule=datamodule)
     trainer.test(model=model, datamodule=datamodule, ckpt_path="best" if not is_debug else None)
diff --git a/scripts/typo_analyze.py b/scripts/typo_analyze.py
new file mode 100644
index 00000000..63accb68
--- /dev/null
+++ b/scripts/typo_analyze.py
@@ -0,0 +1,61 @@
+import sys
+
+import hydra
+import pytorch_lightning as pl
+from dotenv import load_dotenv
+from omegaconf import DictConfig
+from pytorch_lightning.callbacks import Callback
+from torch.utils.data import DataLoader
+
+from jula.cli.utils import suppress_debug_info
+from jula.datamodule.datasets.typo_text_dataset import TypoTextDataset
+from jula.models.typo_module import TypoModule
+
+suppress_debug_info()
+
+
+@hydra.main(version_base=None, config_path="../configs", config_name="typo_corrector")
+def main(cfg: DictConfig):
+    load_dotenv()
+    if isinstance(cfg.devices, str):
+        try:
+            cfg.devices = [int(x) for x in cfg.devices.split(",")]
+        except ValueError:
+            cfg.devices = None
+
+    callbacks: list[Callback] = []
+    for k, v in cfg.get("callbacks", {}).items():
+        if k == "prediction_writer":
+            callbacks.append(hydra.utils.instantiate(v, use_stdout=True))
+
+    trainer: pl.Trainer = hydra.utils.instantiate(
+        cfg.trainer,
+        logger=None,
+        enable_progress_bar=False,
+        callbacks=callbacks,
+        devices=cfg.devices,
+    )
+
+    if cfg.config_name in cfg.module.typo:
+        model = TypoModule.load_from_checkpoint(checkpoint_path=cfg.checkpoint_path, hparams=cfg)
+    else:
+        raise ValueError("invalid config name")
+
+    # TODO: Use hydra for configuration
+    dataset = TypoTextDataset(
+        sys.stdin.readlines(),
+        model_name_or_path=cfg.datamodule.model_name_or_path,
+        max_seq_length=cfg.datamodule.max_seq_length,
+        tokenizer_kwargs=cfg.datamodule.tokenizer_kwargs,
+    )
+    dataloader = DataLoader(
+        dataset,
+        batch_size=cfg.batch_size,
+        shuffle=False,
+        pin_memory=True,
+    )
+    trainer.predict(model=model, dataloaders=dataloader)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/word_analyze.py b/scripts/word_analyze.py
index 557159ca..14ebaa07 100644
--- a/scripts/word_analyze.py
+++ b/scripts/word_analyze.py
@@ -1,19 +1,17 @@
-from typing import Union
+import sys
 
 import hydra
 import pytorch_lightning as pl
-import transformers.utils.logging as hf_logging
 from dotenv import load_dotenv
 from omegaconf import DictConfig
 from pytorch_lightning.callbacks import Callback
 from torch.utils.data import DataLoader
 
+from jula.cli.utils import suppress_debug_info
 from jula.datamodule.datasets.segmented_text_dataset import SegmentedTextDataset
-from jula.models.char_module import CharModule
-from jula.models.typo_module import TypoModule
 from jula.models.word_module import WordModule
 
-hf_logging.set_verbosity(hf_logging.ERROR)
+suppress_debug_info()
 
 
 @hydra.main(version_base=None, config_path="../configs", config_name="word_module")
@@ -33,22 +31,19 @@ def main(cfg: DictConfig):
     trainer: pl.Trainer = hydra.utils.instantiate(
         cfg.trainer,
         logger=None,
+        enable_progress_bar=False,
         callbacks=callbacks,
         devices=cfg.devices,
     )
 
-    model: Union[TypoModule, CharModule, WordModule]
-    # add config name if you want
     if cfg.config_name in cfg.module.word:
         model = WordModule.load_from_checkpoint(checkpoint_path=cfg.checkpoint_path, hparams=cfg)
     else:
         raise ValueError("invalid config name")
 
-    inp = input()
-
     # TODO: Use hydra for configuration
     dataset = SegmentedTextDataset(
-        inp.split("\n"),
+        sys.stdin.readlines(),
         model_name_or_path=cfg.datamodule.model_name_or_path,
         max_seq_length=cfg.datamodule.max_seq_length,
         tokenizer_kwargs=cfg.datamodule.tokenizer_kwargs,
diff --git a/src/jula/callbacks/typo_corrector_writer.py b/src/jula/callbacks/typo_corrector_writer.py
index 72eebb24..9afa9e3d 100644
--- a/src/jula/callbacks/typo_corrector_writer.py
+++ b/src/jula/callbacks/typo_corrector_writer.py
@@ -1,4 +1,3 @@
-import json
 import os
 from pathlib import Path
 from typing import Any, Optional, Sequence
@@ -7,10 +6,9 @@
 import pytorch_lightning as pl
 import torch
 from pytorch_lightning.callbacks import BasePredictionWriter
-from transformers import AutoTokenizer, PreTrainedTokenizer
+from transformers import AutoTokenizer, PreTrainedTokenizerBase
 
-from jula.evaluators.typo_corrector import TypoCorrectorMetric
-from jula.utils.utils import TOKEN2TYPO_OPN, TYPO_OPN2TOKEN
+from jula.utils.constants import TOKEN2TYPO_OPN, TYPO_OPN2TOKEN
 
 
 class TypoCorrectorWriter(BasePredictionWriter):
@@ -29,17 +27,15 @@ def __init__(
         if self.use_stdout:
             self.output_path = ""
         else:
-            self.output_path = f"{output_dir}/{pred_filename}.json"
+            self.output_path = f"{output_dir}/{pred_filename}.txt"
             os.makedirs(output_dir, exist_ok=True)
             if os.path.isfile(self.output_path):
                 os.remove(self.output_path)
 
-        self.tokenizer: PreTrainedTokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             model_name_or_path,
-            **hydra.utils.instantiate(tokenizer_kwargs, _convert_="partial"),
+            **hydra.utils.instantiate(tokenizer_kwargs or {}, _convert_="partial"),
         )
-        self.predicts: dict[int, Any] = dict()
-        self.metrics: TypoCorrectorMetric = TypoCorrectorMetric()
 
         self.opn2id, self.id2opn = self.get_opn_dict(path=Path(extended_vocab_path))
 
@@ -53,81 +49,74 @@ def get_opn_dict(self, path: Path) -> tuple[dict[str, int], dict[int, str]]:
                 id2opn[len(id2opn)] = opn
         return opn2id, id2opn
 
-    def get_opn(
-        self,
-        pred_ids_list: list[list[int]],
-        label_ids_list: list[list[int]],
-        opn_prefix: str,
-    ) -> tuple[list[list[str]], list[list[str]]]:
-        preds_list: list[list[str]] = []
-        labels_list: list[list[str]] = []
-        for pred_ids, label_ids in zip(pred_ids_list, label_ids_list):
-            preds: list[str] = []
-            labels: list[str] = []
-            for pred_id, label_id in zip(pred_ids, label_ids):
-                if label_id == self.tokenizer.pad_token_id:
-                    continue
-
-                if self.id2opn[pred_id] in TYPO_OPN2TOKEN.values():
-                    preds.append(TOKEN2TYPO_OPN[self.id2opn[pred_id]])
-                else:
-                    preds.append(f"{opn_prefix}:{self.id2opn[pred_id]}")
-
-                if self.id2opn[label_id] in TYPO_OPN2TOKEN.values():
-                    labels.append(TOKEN2TYPO_OPN[self.id2opn[label_id]])
+    def convert_id2opn(self, opn_ids_list: list[list[int]], opn_prefix: str) -> list[list[str]]:
+        opns_list: list[list[str]] = []
+        for opn_ids in opn_ids_list:
+            opns: list[str] = []
+            for opn_id in opn_ids:
+                if self.id2opn[opn_id] in TYPO_OPN2TOKEN.values():
+                    opns.append(TOKEN2TYPO_OPN[self.id2opn[opn_id]])
                 else:
-                    labels.append(f"{opn_prefix}:{self.id2opn[label_id]}")
-            preds_list.append(preds)
-            labels_list.append(labels)
-        return preds_list, labels_list
+                    opns.append(f"{opn_prefix}:{self.id2opn[opn_id]}")
+            opns_list.append(opns)
+        return opns_list
 
-    def write_on_batch_end(
-        self,
-        trainer: pl.Trainer,
-        pl_module: pl.LightningModule,
-        prediction: Any,
-        batch_indices: Optional[Sequence[int]],
-        batch: Any,
-        batch_idx: int,
-        dataloader_idx: int,
-    ) -> None:
-        pass
+    @staticmethod
+    def apply_opn(pre_text: str, kdrs: list[str], inss: list[str]) -> str:
+        post_text = ""
+        assert len(pre_text) + 1 == len(kdrs) + 1 == len(inss)
+        for char_idx, char in enumerate(pre_text):
+            # insert
+            if inss[char_idx] != "_":
+                post_text += inss[char_idx].removeprefix("I:")
+            # keep, delete, replace
+            if kdrs[char_idx] == "K":
+                post_text += char
+            elif kdrs[char_idx] == "D":
+                pass
+            elif kdrs[char_idx].startswith("R:"):
+                post_text += kdrs[char_idx].removeprefix("R:")
+            else:
+                raise ValueError("unsupported operation!")
+        if inss[-1] != "_":
+            post_text += inss[-1].removeprefix("I:")
+        return post_text
 
     def write_on_epoch_end(
         self,
         trainer: pl.Trainer,
         pl_module: pl.LightningModule,
         predictions: Sequence[Any],
-        batch_indices: Optional[Sequence[Any]],
+        batch_indices: Optional[Sequence[Any]] = None,
     ) -> None:
-        example_id = 0
+        results = []
         for prediction in predictions:
             for batch_pred in prediction:
-                kdr_preds, kdr_labels = self.get_opn(
-                    pred_ids_list=torch.argmax(batch_pred["kdr_logits"], dim=-1).tolist(),
-                    label_ids_list=batch_pred["kdr_labels"].tolist(),
+                # the prediction of the first token (= [CLS]) is excluded.
+                kdr_preds: list[list[str]] = self.convert_id2opn(
+                    opn_ids_list=torch.argmax(batch_pred["kdr_logits"][:, 1:], dim=-1).tolist(),
                     opn_prefix="R",
                 )
-                ins_preds, ins_labels = self.get_opn(
-                    pred_ids_list=torch.argmax(batch_pred["ins_logits"], dim=-1).tolist(),
-                    label_ids_list=batch_pred["ins_labels"].tolist(),
+                ins_preds: list[list[str]] = self.convert_id2opn(
+                    opn_ids_list=torch.argmax(batch_pred["ins_logits"][:, 1:], dim=-1).tolist(),
                     opn_prefix="I",
                 )
-                for idx in range(len(batch_pred["input_ids"])):
-                    self.predicts[example_id] = dict(
-                        input_ids=self.tokenizer.decode(
-                            [x for x in batch_pred["input_ids"][idx]][:-1],
-                            skip_special_tokens=True,
-                        ),
-                        kdr_preds=kdr_preds[idx],
-                        kdr_labels=kdr_labels[idx],
-                        ins_preds=ins_preds[idx],
-                        ins_labels=ins_labels[idx],
+                result = []
+                for idx in range(len(batch_pred["kdr_logits"])):
+                    seq_len: int = len(batch_pred["texts"][idx])
+                    # the prediction of the dummy token (= "<dummy>") at the end of the input is used for insertion only.
+                    result.append(
+                        self.apply_opn(
+                            pre_text=batch_pred["texts"][idx],
+                            kdrs=kdr_preds[idx][:seq_len],
+                            inss=ins_preds[idx][: seq_len + 1],
+                        )
                     )
-                    example_id += 1
+                results.append("\n".join(result))
 
+        out = "\n".join(results)
         if self.use_stdout:
-            print(json.dumps(self.predicts, ensure_ascii=False, indent=2))
+            print(out)
         else:
             with open(self.output_path, "w") as f:
-                json.dump(self.predicts, f, ensure_ascii=False, indent=2)
+                f.write(out)
diff --git a/src/jula/callbacks/word_module_writer.py b/src/jula/callbacks/word_module_writer.py
index 16cbe9cf..aea98d43 100644
--- a/src/jula/callbacks/word_module_writer.py
+++ b/src/jula/callbacks/word_module_writer.py
@@ -1,25 +1,32 @@
-import json
+import logging
 import os
-from collections import defaultdict
-from typing import Any, Optional, Sequence
+import sys
+from pathlib import Path
+from typing import Any, Optional, Sequence, TextIO, Union
 
 import hydra
 import pytorch_lightning as pl
 import torch
 from pytorch_lightning.callbacks import BasePredictionWriter
-from rhoknp import Morpheme
+from rhoknp import BasePhrase, Document, Morpheme, Phrase, Sentence
+from rhoknp.rel import ExophoraReferent
+from rhoknp.units.morpheme import MorphemeAttributes
+from rhoknp.units.utils import Features, Rel, Rels, Semantics
 from transformers import AutoTokenizer, PreTrainedTokenizerBase
 
-from jula.utils.utils import (
+import jula
+from jula.utils.constants import (
     BASE_PHRASE_FEATURES,
     INDEX2CONJFORM_TYPE,
     INDEX2CONJTYPE_TYPE,
     INDEX2DEPENDENCY_TYPE,
+    INDEX2DISCOURSE_RELATION,
     INDEX2POS_TYPE,
     INDEX2SUBPOS_TYPE,
-    WORD_FEATURES,
 )
 
+logger = logging.getLogger(__name__)
+
 
 class WordModuleWriter(BasePredictionWriter):
     def __init__(
@@ -28,185 +35,260 @@ def __init__(
         pred_filename: str = "predict",
         model_name_or_path: str = "nlp-waseda/roberta-base-japanese",
         tokenizer_kwargs: dict = None,
+        max_seq_length: int = 512,
         use_stdout: bool = False,
     ) -> None:
         super().__init__(write_interval="epoch")
 
-        self.use_stdout = use_stdout
-        if self.use_stdout:
-            self.output_path = ""
+        self.destination: Union[Path, TextIO]
+        if use_stdout is True:
+            self.destination = sys.stdout
         else:
-            self.output_path = f"{output_dir}/{pred_filename}.json"
-            os.makedirs(output_dir, exist_ok=True)
-            if os.path.isfile(self.output_path):
-                os.remove(self.output_path)
+            self.destination = Path(f"{output_dir}/{pred_filename}.knp")
+            self.destination.parent.mkdir(exist_ok=True)
+            if self.destination.exists():
+                os.remove(str(self.destination))
 
         self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             model_name_or_path,
-            **hydra.utils.instantiate(tokenizer_kwargs, _convert_="partial"),
+            **hydra.utils.instantiate(tokenizer_kwargs or {}, _convert_="partial"),
         )
         self.pad_token_id = self.tokenizer.pad_token_id
-
-    def write_on_batch_end(
-        self,
-        trainer: pl.Trainer,
-        pl_module: pl.LightningModule,
-        prediction: Any,
-        batch_indices: Optional[Sequence[int]],
-        batch: Any,
-        batch_idx: int,
-        dataloader_idx: int,
-    ) -> None:
-        pass
+        self.max_seq_length = max_seq_length
+        # cohesion analysis settings
+        # TODO: fix hard coding
+        self.relations = ["ガ", "ヲ", "ニ", "ガ２", "ノ", "="]
+        self.exophora_referents: list[ExophoraReferent] = [ExophoraReferent(s) for s in ("著者", "読者", "不特定:人", "不特定:物")]
+        self.special_tokens: list[str] = [str(e) for e in self.exophora_referents] + [
+            "[NULL]",
+            "[NA]",
+            "[ROOT]",
+        ]
+        self.special_to_index: dict[str, int] = {
+            token: self.max_seq_length - len(self.special_tokens) + i for i, token in enumerate(self.special_tokens)
+        }
+        self.index_to_special: dict[int, str] = {v: k for k, v in self.special_to_index.items()}
 
     def write_on_epoch_end(
         self,
         trainer: pl.Trainer,
         pl_module: pl.LightningModule,
         predictions: Sequence[Any],
-        batch_indices: Optional[Sequence[Any]],
+        batch_indices: Optional[Sequence[Any]] = None,
     ) -> None:
-        results = defaultdict(list)
-        for dataloader_idx, prediction_step_outputs in enumerate(predictions):
-            corpus = pl_module.test_corpora[dataloader_idx]
-            dataset = trainer.datamodule.test_datasets[corpus]
-            for prediction_step_output in prediction_step_outputs:
-                batch_pos_preds = torch.argmax(prediction_step_output["word_analysis_pos_logits"], dim=-1)
-                batch_subpos_preds = torch.argmax(prediction_step_output["word_analysis_subpos_logits"], dim=-1)
-                batch_conjtype_preds = torch.argmax(prediction_step_output["word_analysis_conjtype_logits"], dim=-1)
-                batch_conjform_preds = torch.argmax(prediction_step_output["word_analysis_conjform_logits"], dim=-1)
-                batch_word_feature_predictions = prediction_step_output["word_feature_logits"].ge(0.5).long()
-                batch_base_phrase_feature_predictions = (
-                    prediction_step_output["base_phrase_feature_logits"].ge(0.5).long()
-                )
-                batch_dependency_predictions = torch.argmax(prediction_step_output["dependency_logits"], dim=2)
-                batch_dependency_type_predictions = torch.argmax(
-                    prediction_step_output["dependency_type_logits"], dim=2
-                )
+        documents: list[Document] = []
+        for prediction in predictions:
+            for batch_pred in prediction:
+                batch_texts = batch_pred["texts"]
+                batch_pos_preds = torch.argmax(batch_pred["word_analysis_pos_logits"], dim=-1)
+                batch_subpos_preds = torch.argmax(batch_pred["word_analysis_subpos_logits"], dim=-1)
+                batch_conjtype_preds = torch.argmax(batch_pred["word_analysis_conjtype_logits"], dim=-1)
+                batch_conjform_preds = torch.argmax(batch_pred["word_analysis_conjform_logits"], dim=-1)
+                batch_word_feature_preds = batch_pred["word_feature_logits"]
+                batch_base_phrase_feature_preds = batch_pred["base_phrase_feature_logits"].ge(0.5).long()
+                batch_dependency_preds = torch.topk(
+                    batch_pred["dependency_logits"],
+                    # pl_module.hparams.k,  # TODO: move to WordModuleWriter's config or argument
+                    k=1,
+                    dim=2,
+                ).indices
+                batch_dependency_type_preds = torch.argmax(batch_pred["dependency_type_logits"], dim=3)
+                batch_cohesion_preds = torch.argmax(batch_pred["cohesion_logits"], dim=3)  # (b, rel, word)
+                batch_discourse_parsing_preds = torch.argmax(batch_pred["discourse_parsing_logits"], dim=3)
                 for (
-                    example_id,
+                    text,
                     pos_preds,
                     subpos_preds,
                     conjtype_preds,
                     conjform_preds,
-                    word_feature_predictions,
-                    base_phrase_feature_predictions,
-                    dependency_predictions,
-                    dependency_type_predictions,
+                    word_feature_preds,
+                    base_phrase_feature_preds,
+                    dependency_preds,
+                    dependency_type_preds,
+                    cohesion_preds,
+                    discourse_parsing_preds,
                 ) in zip(
-                    prediction_step_output["example_ids"],
+                    batch_texts,
                     batch_pos_preds.tolist(),
                     batch_subpos_preds.tolist(),
                     batch_conjtype_preds.tolist(),
                     batch_conjform_preds.tolist(),
-                    batch_word_feature_predictions.tolist(),
-                    batch_base_phrase_feature_predictions.tolist(),
-                    batch_dependency_predictions.tolist(),
-                    batch_dependency_type_predictions.tolist(),
+                    batch_word_feature_preds.tolist(),
+                    batch_base_phrase_feature_preds.tolist(),
+                    batch_dependency_preds.tolist(),
+                    batch_dependency_type_preds.tolist(),
+                    batch_cohesion_preds.tolist(),
+                    batch_discourse_parsing_preds.tolist(),
                 ):
-                    document = dataset.documents[example_id]
-                    results[corpus].append(
-                        [
-                            self.convert_predictions(values, len(dependency_predictions))
-                            for values in zip(
-                                document.morphemes,
-                                pos_preds,
-                                subpos_preds,
-                                conjtype_preds,
-                                conjform_preds,
-                                word_feature_predictions,
-                                base_phrase_feature_predictions,
-                                dependency_predictions,
-                                dependency_type_predictions,
-                            )
-                        ]
+                    morphemes = self._create_morphemes(
+                        text.split(),
+                        pos_preds,
+                        subpos_preds,
+                        conjtype_preds,
+                        conjform_preds,
                     )
+                    document = self._chunk_morphemes(morphemes, word_feature_preds)
+                    self._add_base_phrase_features(document, base_phrase_feature_preds)
+                    self._add_dependency(document, dependency_preds, dependency_type_preds)
+                    self._add_cohesion(document, cohesion_preds)
+                    document = Document.from_knp(document.to_knp())  # reparse to get clauses
+                    self._add_discourse(document, discourse_parsing_preds)
+                    documents.append(document)
 
-        if self.use_stdout:
-            print(json.dumps(results, ensure_ascii=False, indent=2))
-        else:
-            with open(self.output_path, "w") as f:
-                json.dump(results, f, ensure_ascii=False, indent=2)
+        output_string = "".join(doc.to_knp() for doc in documents)
+        if isinstance(self.destination, Path):
+            self.destination.write_text(output_string)
+        elif isinstance(self.destination, TextIO):
+            self.destination.write(output_string)
 
     @staticmethod
-    def convert_word_analysis_pred(
-        morpheme: Morpheme,
-        pos_pred: int,
-        subpos_pred: int,
-        conjtype_pred: int,
-        conjform_pred: int,
-    ):
-        pred = (
-            f"{INDEX2POS_TYPE[pos_pred]} {INDEX2SUBPOS_TYPE[subpos_pred]} "
-            f"{INDEX2CONJTYPE_TYPE[conjtype_pred]} {INDEX2CONJFORM_TYPE[conjform_pred]}"
-        )
-        label = f"{morpheme.pos} {morpheme.subpos} {morpheme.conjtype} {morpheme.conjform}"
-        return f"{pred}|{label}"
+    def _create_morphemes(
+        words: list[str],
+        pos_preds: list[int],
+        subpos_preds: list[int],
+        conjtype_preds: list[int],
+        conjform_preds: list[int],
+    ) -> list[Morpheme]:
+        morphemes = []
+        for word, pos_index, subpos_index, conjtype_index, conjform_index in zip(
+            words, pos_preds, subpos_preds, conjtype_preds, conjform_preds
+        ):
+            attributes = MorphemeAttributes(
+                surf=word,
+                reading=word,  # TODO
+                lemma=word,  # TODO
+                pos=INDEX2POS_TYPE[pos_index],
+                pos_id=0,  # TODO
+                subpos=INDEX2SUBPOS_TYPE[subpos_index],
+                subpos_id=0,  # TODO
+                conjtype=INDEX2CONJTYPE_TYPE[conjtype_index],
+                conjtype_id=0,  # TODO
+                conjform=INDEX2CONJFORM_TYPE[conjform_index],
+                conjform_id=0,  # TODO
+            )
+            morphemes.append(Morpheme(attributes, Semantics(), Features()))
+        return morphemes
 
     @staticmethod
-    def convert_phrase_analysis_prediction(
-        word_feature_prediction,
-        base_phrase_feature_prediction,
-        is_base_phrase_head: bool,
-    ):
-        word_features = "".join(
-            f"<{feature}>" for feature, pred in zip(WORD_FEATURES, word_feature_prediction) if pred == 1
-        )
-        if not is_base_phrase_head:
-            return f"{word_features}|"
-        else:
-            base_phrase_features = "".join(
-                f"<{feature}>"
-                for feature, pred in zip(BASE_PHRASE_FEATURES, base_phrase_feature_prediction)
-                if pred == 1
-            )
-            return f"{word_features}|{base_phrase_features}"
+    def _chunk_morphemes(morphemes: list[Morpheme], word_feature_preds: list[list[float]]) -> Document:
+        phrases_buff = []
+        base_phrases_buff = []
+        morphemes_buff = []
+        assert len(morphemes) <= len(word_feature_preds)
+        for i, (morpheme, word_feature_pred) in enumerate(zip(morphemes, word_feature_preds)):
+            morphemes_buff.append(morpheme)
+            # follows WORD_FEATURES
+            base_phrase_head_prob, base_phrase_end_prob, phrase_end_prob = word_feature_pred
+            if base_phrase_head_prob >= 0.5:
+                morpheme.features["基本句-主辞"] = True
+            # even if base_phrase_end_prob is low, if phrase_end_prob is high enough, create chunk here
+            if base_phrase_end_prob >= 0.5 or base_phrase_end_prob + phrase_end_prob >= 1.0:
+                base_phrase = BasePhrase(None, None, Features(), Rels())
+                base_phrase.morphemes = morphemes_buff
+                morphemes_buff = []
+                base_phrases_buff.append(base_phrase)
+            # even if phrase_end_prob is high, if base_phrase_end_prob is not high enough, do not create chunk here
+            if phrase_end_prob >= 0.5 and base_phrase_end_prob + phrase_end_prob >= 1.0:
+                phrase = Phrase(None, None, Features())
+                phrase.base_phrases = base_phrases_buff
+                base_phrases_buff = []
+                phrases_buff.append(phrase)
+        # clear buffers
+        if morphemes_buff:
+            base_phrase = BasePhrase(None, None, Features(), Rels())
+            base_phrase.morphemes = morphemes_buff
+            base_phrases_buff.append(base_phrase)
+        if base_phrases_buff:
+            phrase = Phrase(None, None, Features())
+            phrase.base_phrases = base_phrases_buff
+            phrases_buff.append(phrase)
+
+        sentence = Sentence()
+        sentence.sid = "1"
+        sentence.misc_comment = f"jula:{jula.__version__}"
+        sentence.phrases = phrases_buff
+        # TODO: support document with multiple sentences
+        return Document.from_sentences([sentence])
 
     @staticmethod
-    def convert_dependency_parsing_prediction(morpheme, dependency_prediction, dependency_type_prediction, seq_len):
-        offset = min(morpheme.global_index for morpheme in morpheme.sentence.morphemes)
-        if dependency_prediction == seq_len - 1:
-            system_head = -1
-            system_deprel = "D"
-        else:
-            system_head = dependency_prediction - offset
-            system_deprel = INDEX2DEPENDENCY_TYPE[dependency_type_prediction]
+    def _add_base_phrase_features(document: Document, base_phrase_feature_preds: list[list[int]]) -> None:
+        for base_phrase in document.base_phrases:
+            for feature, pred in zip(BASE_PHRASE_FEATURES, base_phrase_feature_preds[base_phrase.head.global_index]):
+                if pred == 1:
+                    k, *vs = feature.split(":")
+                    base_phrase.features[k] = ":".join(vs) or True
 
-        if morpheme == morpheme.base_phrase.head:
-            gold_head = morpheme.parent.index if morpheme.parent else -1
-            gold_deprel = morpheme.base_phrase.dep_type.value if morpheme.parent else "ROOT"
-            return f"{system_head}{system_deprel}|{gold_head}{gold_deprel}"
-        else:
-            gold_head = morpheme.base_phrase.head.index
-            gold_deprel = "D"
-            return f"{system_head}{system_deprel}|{gold_head}{gold_deprel}"
-
-    def convert_predictions(self, values, seq_len):
-        (
-            morpheme,
-            pos_pred,
-            subpos_pred,
-            conjtype_pred,
-            conjform_pred,
-            word_feature_prediction,
-            base_phrase_feature_prediction,
-            dependency_prediction,
-            dependency_type_prediction,
-        ) = values
-        id_, surf = morpheme.index, morpheme.surf
-        word_analysis_result = self.convert_word_analysis_pred(
-            morpheme,
-            pos_pred,
-            subpos_pred,
-            conjtype_pred,
-            conjform_pred,
-        )
-        phrase_analysis_result = self.convert_phrase_analysis_prediction(
-            word_feature_prediction,
-            base_phrase_feature_prediction,
-            morpheme == morpheme.base_phrase.head,
-        )
-        dependency_parsing_result = self.convert_dependency_parsing_prediction(
-            morpheme, dependency_prediction, dependency_type_prediction, seq_len
-        )
-        return f"{id_}|{surf}|{word_analysis_result}|{phrase_analysis_result}|{dependency_parsing_result}"
+    def _add_dependency(
+        self, document: Document, dependency_preds: list[list[int]], dependency_type_preds: list[list[int]]
+    ) -> None:
+        morphemes = document.morphemes
+        for base_phrase in document.base_phrases:
+            parent_morpheme_index = dependency_preds[base_phrase.head.global_index][0]
+            dependency_type_id = dependency_type_preds[base_phrase.head.global_index][0]
+            if 0 <= parent_morpheme_index < len(morphemes):
+                base_phrase.parent_index = morphemes[parent_morpheme_index].base_phrase.index
+            elif parent_morpheme_index == self.special_to_index["[ROOT]"]:
+                base_phrase.parent_index = -1
+            base_phrase.dep_type = INDEX2DEPENDENCY_TYPE[dependency_type_id]
+
+    def _add_cohesion(self, document: Document, cohesion_preds: list[list[int]]) -> None:
+        for base_phrase in document.base_phrases:
+            base_phrase.rels = self._to_rels(
+                [preds[base_phrase.head.global_index] for preds in cohesion_preds], document.morphemes
+            )
+
+    def _add_discourse(self, document: Document, discourse_preds: list[list[int]]) -> None:
+        if document.need_clause_tag:
+            logger.warning("failed to output clause boundaries")
+            return
+        for modifier in document.clauses:
+            modifier_morpheme_index = modifier.end.morphemes[0].global_index
+            preds = []
+            for head in document.clauses:
+                head_sid = head.sentence.sid
+                head_morpheme_index = head.end.morphemes[0].global_index
+                head_base_phrase_index = head.end.index
+                pred = INDEX2DISCOURSE_RELATION[discourse_preds[modifier_morpheme_index][head_morpheme_index]]
+                if pred != "談話関係なし":
+                    preds.append(f"{head_sid}/{head_base_phrase_index}/{pred}")
+            if preds:
+                modifier.end.features["談話関係"] = ";".join(preds)
+
+    def _to_rels(
+        self,
+        prediction: list[int],  # (rel)
+        morphemes: list[Morpheme],
+    ) -> Rels:
+        rels = Rels()
+        assert len(self.relations) == len(prediction)
+        for relation, morpheme_index in zip(self.relations, prediction):
+            if morpheme_index < 0:
+                continue  # non-target phrase
+            if 0 <= morpheme_index < len(morphemes):
+                # normal
+                prediction_bp: BasePhrase = morphemes[morpheme_index].base_phrase
+                rels.append(
+                    Rel(
+                        type=relation,
+                        target=prediction_bp.head.text,
+                        sid=prediction_bp.sentence.sid,
+                        base_phrase_index=prediction_bp.index,
+                        mode=None,
+                    )
+                )
+            elif special_token := self.index_to_special.get(morpheme_index):
+                # exophora
+                if special_token in [str(e) for e in self.exophora_referents]:  # exclude [NULL], [NA], and [ROOT]
+                    rels.append(
+                        Rel(
+                            type=relation,
+                            target=special_token,
+                            sid=None,
+                            base_phrase_index=None,
+                            mode=None,
+                        )
+                    )
+            else:
+                raise ValueError(f"invalid morpheme index: {morpheme_index} in {morphemes[0].document.doc_id}")
+
+        return rels
diff --git a/src/jula/callbacks/word_segmenter_writer.py b/src/jula/callbacks/word_segmenter_writer.py
index d59d71b5..32acbe6b 100644
--- a/src/jula/callbacks/word_segmenter_writer.py
+++ b/src/jula/callbacks/word_segmenter_writer.py
@@ -1,12 +1,13 @@
 import os
 from typing import Any, Optional, Sequence
 
+import hydra
 import pytorch_lightning as pl
 import torch
 from pytorch_lightning.callbacks import BasePredictionWriter
-from transformers import AutoTokenizer, PreTrainedTokenizer
+from transformers import AutoTokenizer, PreTrainedTokenizerBase
 
-from jula.evaluators.word_segmenter import WordSegmenterMetric
+from jula.utils.constants import INDEX2SEG_TYPE
 
 
 class WordSegmenterWriter(BasePredictionWriter):
@@ -24,56 +25,45 @@ def __init__(
         if self.use_stdout:
             self.output_path = ""
         else:
-            self.output_path = f"{output_dir}/{pred_filename}.json"
+            self.output_path = f"{output_dir}/{pred_filename}.txt"
             os.makedirs(output_dir, exist_ok=True)
             if os.path.isfile(self.output_path):
                 os.remove(self.output_path)
 
-        self.tokenizer: PreTrainedTokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             model_name_or_path,
-            **tokenizer_kwargs,
+            **hydra.utils.instantiate(tokenizer_kwargs or {}, _convert_="partial"),
         )
-        self.metrics: WordSegmenterMetric = WordSegmenterMetric()
-
-    def write_on_batch_end(
-        self,
-        trainer: pl.Trainer,
-        pl_module: pl.LightningModule,
-        prediction: Any,
-        batch_indices: Optional[Sequence[int]],
-        batch: Any,
-        batch_idx: int,
-        dataloader_idx: int,
-    ) -> None:
-        pass
 
     def write_on_epoch_end(
         self,
         trainer: pl.Trainer,
         pl_module: pl.LightningModule,
         predictions: Sequence[Any],
-        batch_indices: Optional[Sequence[Any]],
+        batch_indices: Optional[Sequence[Any]] = None,
     ) -> None:
         results = []
         for prediction in predictions:
             for batch_pred in prediction:
-                seg_preds = [
-                    self.metrics.convert_ids_to_labels(ids)
-                    for ids in torch.argmax(batch_pred["logits"], dim=-1).cpu().tolist()
-                ]  # (b, seq_len)
-                for item_index in range(len(batch_pred["input_ids"])):
+                batch_size = len(batch_pred["input_ids"])
+                for i in range(batch_size):
+                    input_ids = batch_pred["input_ids"][i].cpu().tolist()  # (seq_len,)
+                    pred_logits = batch_pred["word_segmenter_logits"][i]  # (seq_len, len(INDEX2SEG_TYPE))
+                    pred_ids = torch.argmax(pred_logits, dim=1).cpu().tolist()  # (seq_len,)
+                    pred_types = [INDEX2SEG_TYPE[id_] for id_ in pred_ids]  # (seq_len,)
+                    assert len(input_ids) == len(pred_types)
                     result = ""
-                    for token_index in range(len(batch_pred["input_ids"][item_index])):
-                        token_id = batch_pred["input_ids"][item_index][token_index]
-                        if token_id in self.tokenizer.all_special_ids:
+                    for input_id, pred_type in zip(input_ids, pred_types):
+                        if input_id in self.tokenizer.all_special_ids:
                             continue
-                        seg_pred = seg_preds[item_index][token_index]
-                        if seg_pred == "B":
+                        if pred_type == "B":
                             result += " "
-                        result += self.tokenizer.decode(token_id)
+                        result += self.tokenizer.decode(input_id)
                     results.append(result.strip())
+
+        out = "\n".join(results)
         if self.use_stdout:
-            print("\n".join(results))
+            print(out)
         else:
             with open(self.output_path, "w") as f:
-                f.write("\n".join(results))
+                f.write(out)
diff --git a/src/jula/cli/utils.py b/src/jula/cli/utils.py
new file mode 100644
index 00000000..05c5deec
--- /dev/null
+++ b/src/jula/cli/utils.py
@@ -0,0 +1,11 @@
+import logging
+import warnings
+
+import transformers.utils.logging as hf_logging
+
+
+def suppress_debug_info() -> None:
+    warnings.filterwarnings("ignore")
+    logging.getLogger("torch").setLevel(logging.ERROR)
+    logging.getLogger("pytorch_lightning").setLevel(logging.ERROR)
+    hf_logging.set_verbosity(hf_logging.ERROR)
diff --git a/src/jula/datamodule/datasets/base_dataset.py b/src/jula/datamodule/datasets/base_dataset.py
index 58c7bbe4..35dc4876 100644
--- a/src/jula/datamodule/datasets/base_dataset.py
+++ b/src/jula/datamodule/datasets/base_dataset.py
@@ -1,10 +1,11 @@
 import sys
+from functools import cached_property
 from pathlib import Path
 
 import hydra
 from rhoknp import Document
 from torch.utils.data import Dataset
-from transformers import AutoTokenizer, PreTrainedTokenizer
+from transformers import AutoTokenizer, PreTrainedTokenizerBase
 
 
 class BaseDataset(Dataset):
@@ -20,15 +21,14 @@ def __init__(
         self.path = Path(path)
         assert self.path.is_dir()
 
-        self.documents = self.load_documents(self.path, ext)
-        assert len(self) != 0
-        self.document_ids: list[str] = [doc.doc_id for doc in self.documents]
+        self.doc_id2document = self.load_documents(self.path, ext)
+        assert len(self.documents) != 0
 
         if tokenizer_kwargs:
             tokenizer_kwargs = hydra.utils.instantiate(tokenizer_kwargs, _convert_="partial")
         else:
             tokenizer_kwargs = {}
-        self.tokenizer: PreTrainedTokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             model_name_or_path,
             **tokenizer_kwargs,
         )
@@ -38,12 +38,17 @@ def __len__(self) -> int:
         return len(self.documents)
 
     @staticmethod
-    def load_documents(path: Path, ext: str = "knp") -> list[Document]:
-        documents = []
-        for file_path in sorted(path.glob(f"**/*.{ext}")):
+    def load_documents(path: Path, ext: str = "knp") -> dict[str, Document]:
+        doc_id2document: dict[str, Document] = {}
+        for file_path in sorted(path.glob(f"*.{ext}")):
             # TODO: fix document file
             try:
-                documents.append(Document.from_knp(file_path.read_text()))
+                document = Document.from_knp(file_path.read_text())
+                doc_id2document[document.doc_id] = document
             except AssertionError:
                 print(f"{file_path} is not a valid knp file.", file=sys.stderr)
-        return documents
+        return doc_id2document
+
+    @cached_property
+    def documents(self) -> list[Document]:
+        return list(self.doc_id2document.values())
diff --git a/src/jula/datamodule/datasets/char_dataset.py b/src/jula/datamodule/datasets/char_dataset.py
index f4eaaaab..eb2ec2e6 100644
--- a/src/jula/datamodule/datasets/char_dataset.py
+++ b/src/jula/datamodule/datasets/char_dataset.py
@@ -7,7 +7,7 @@
 from transformers.utils import PaddingStrategy
 
 from jula.datamodule.datasets.base_dataset import BaseDataset
-from jula.utils.utils import ENE_TYPE_BIES, IGNORE_INDEX, SEG_TYPES
+from jula.utils.constants import ENE_TYPE_BIES, IGNORE_INDEX, SEG_TYPES
 
 
 class CharDataset(BaseDataset):
diff --git a/src/jula/datamodule/datasets/raw_text_dataset.py b/src/jula/datamodule/datasets/raw_text_dataset.py
index 63588edb..3baeef9a 100644
--- a/src/jula/datamodule/datasets/raw_text_dataset.py
+++ b/src/jula/datamodule/datasets/raw_text_dataset.py
@@ -1,7 +1,7 @@
 import hydra
 import torch
 from torch.utils.data import Dataset
-from transformers import AutoTokenizer, BatchEncoding, PreTrainedTokenizer
+from transformers import AutoTokenizer, BatchEncoding, PreTrainedTokenizerBase
 from transformers.utils import PaddingStrategy
 
 
@@ -13,12 +13,12 @@ def __init__(
         max_seq_length: int = 512,
         tokenizer_kwargs: dict = None,
     ) -> None:
-        self.texts = texts
+        self.texts = [text.strip() for text in texts]
         if tokenizer_kwargs:
             tokenizer_kwargs = hydra.utils.instantiate(tokenizer_kwargs, _convert_="partial")
         else:
             tokenizer_kwargs = {}
-        self.tokenizer: PreTrainedTokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             model_name_or_path,
             **tokenizer_kwargs,
         )
diff --git a/src/jula/datamodule/datasets/segmented_text_dataset.py b/src/jula/datamodule/datasets/segmented_text_dataset.py
index 94aadf3f..5412a397 100644
--- a/src/jula/datamodule/datasets/segmented_text_dataset.py
+++ b/src/jula/datamodule/datasets/segmented_text_dataset.py
@@ -1,9 +1,13 @@
 import hydra
 import torch
+from rhoknp.rel import ExophoraReferent
+from tokenizers import Encoding
 from torch.utils.data import Dataset
-from transformers import AutoTokenizer, BatchEncoding, PreTrainedTokenizer
+from transformers import AutoTokenizer, PreTrainedTokenizerBase
 from transformers.utils import PaddingStrategy
 
+from jula.datamodule.examples import Task
+
 
 class SegmentedTextDataset(Dataset):
     def __init__(
@@ -13,43 +17,87 @@ def __init__(
         max_seq_length: int = 512,
         tokenizer_kwargs: dict = None,
     ) -> None:
-        self.texts = texts
+        self.texts = [text.strip() for text in texts]
+        self.exophora_referents: list[ExophoraReferent] = [ExophoraReferent(s) for s in ["著者", "読者", "不特定:人", "不特定:物"]]
+        self.special_tokens: list[str] = [str(e) for e in self.exophora_referents] + [
+            "[NULL]",
+            "[NA]",
+            "[ROOT]",  # TODO: mask in cohesion analysis
+        ]
         if tokenizer_kwargs:
             tokenizer_kwargs = hydra.utils.instantiate(tokenizer_kwargs, _convert_="partial")
         else:
             tokenizer_kwargs = {}
-        self.tokenizer: PreTrainedTokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             model_name_or_path,
             **tokenizer_kwargs,
         )
         self.max_seq_length = max_seq_length
+        self.special_to_index: dict[str, int] = {
+            token: self.max_seq_length - len(self.special_tokens) + i for i, token in enumerate(self.special_tokens)
+        }
+        self.cohesion_tasks: list[Task] = [Task(t) for t in ["pas_analysis", "bridging", "coreference"]]
+        self.rels = ["ガ", "ヲ", "ニ", "ガ２", "ノ", "="]
+
+    @property
+    def special_indices(self) -> list[int]:
+        return list(self.special_to_index.values())
+
+    @property
+    def num_special_tokens(self) -> int:
+        return len(self.special_tokens)
 
     def __len__(self) -> int:
         return len(self.texts)
 
     def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
-        return {
-            "example_ids": torch.tensor(index, dtype=torch.long),
-            **self.encode(self.texts[index]),
-        }
+        return self.encode(self.texts[index], index)
 
-    def encode(self, text: str) -> dict[str, torch.Tensor]:
-        encoding: BatchEncoding = self.tokenizer(
+    def encode(self, text: str, example_id: int) -> dict[str, torch.Tensor]:
+        encoding: Encoding = self.tokenizer(
             text,
             truncation=True,
             padding=PaddingStrategy.MAX_LENGTH,
-            max_length=self.max_seq_length - 1,
-        )
-        input_ids = encoding["input_ids"] + [self.tokenizer.vocab["[ROOT]"]]
-        attention_mask = encoding["attention_mask"] + [1]
-        subword_map = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
-        for token_id, word_id in enumerate(encoding.word_ids()):
-            if word_id is not None:
-                subword_map[word_id][token_id] = True
-        intra_mask = [[True] * self.max_seq_length for _ in range(self.max_seq_length)]
+            max_length=self.max_seq_length - self.num_special_tokens,
+        ).encodings[0]
+
+        intra_mask = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
+        num_morphemes = len(text.split(" "))
+        for i in range(0, num_morphemes):
+            for j in range(0, num_morphemes):
+                if i != j:
+                    intra_mask[i][j] = True
+            intra_mask[i][-1] = True
+        cohesion_mask = [True] * num_morphemes
+        cohesion_mask += [False] * (self.max_seq_length - num_morphemes - self.num_special_tokens)
+        cohesion_mask += [True] * self.num_special_tokens
+
+        special_encoding: Encoding = self.tokenizer(
+            self.special_tokens,
+            is_split_into_words=True,
+            padding=PaddingStrategy.DO_NOT_PAD,
+            truncation=False,
+            add_special_tokens=False,
+        ).encodings[0]
+        merged_encoding: Encoding = Encoding.merge([encoding, special_encoding])
+
         return {
-            "input_ids": torch.tensor(input_ids, dtype=torch.long),
-            "attention_mask": torch.tensor(attention_mask, dtype=torch.long),
-            "subword_map": torch.tensor(subword_map, dtype=torch.bool),
+            "example_ids": torch.tensor(example_id, dtype=torch.long),
+            "input_ids": torch.tensor(merged_encoding.ids, dtype=torch.long),
+            "attention_mask": torch.tensor(merged_encoding.attention_mask, dtype=torch.long),
+            "subword_map": torch.tensor(self._gen_subword_map(merged_encoding), dtype=torch.bool),
             "intra_mask": torch.tensor(intra_mask, dtype=torch.bool),
+            "cohesion_mask": torch.tensor(cohesion_mask, dtype=torch.bool)
+            .view(1, 1, -1)
+            .expand(len(self.rels), self.max_seq_length, self.max_seq_length),
+            "texts": text,
         }
+
+    def _gen_subword_map(self, encoding: Encoding) -> list[list[bool]]:
+        subword_map = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
+        for token_id, word_id in enumerate(encoding.word_ids):
+            if word_id is not None:
+                subword_map[word_id][token_id] = True
+        for special_index in self.special_indices:
+            subword_map[special_index][special_index] = True
+        return subword_map
diff --git a/src/jula/datamodule/datasets/typo_dataset.py b/src/jula/datamodule/datasets/typo_dataset.py
index 1dd97da1..ef66fb1c 100644
--- a/src/jula/datamodule/datasets/typo_dataset.py
+++ b/src/jula/datamodule/datasets/typo_dataset.py
@@ -5,10 +5,10 @@
 import hydra
 import torch
 from torch.utils.data import Dataset
-from transformers import AutoTokenizer, BatchEncoding, PreTrainedTokenizer
+from transformers import AutoTokenizer, BatchEncoding, PreTrainedTokenizerBase
 from transformers.utils import PaddingStrategy
 
-from jula.utils.utils import TYPO_DUMMY_TOKEN, TYPO_OPN2TOKEN
+from jula.utils.constants import TYPO_DUMMY_TOKEN, TYPO_OPN2TOKEN
 
 
 class TypoDataset(Dataset):
@@ -27,7 +27,7 @@ def __init__(
         assert len(self) != 0
 
         tokenizer_kwargs = tokenizer_kwargs or {}
-        self.tokenizer: PreTrainedTokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             model_name_or_path,
             **hydra.utils.instantiate(tokenizer_kwargs, _convert_="partial"),
         )
diff --git a/src/jula/datamodule/datasets/typo_text_dataset.py b/src/jula/datamodule/datasets/typo_text_dataset.py
new file mode 100644
index 00000000..7026106e
--- /dev/null
+++ b/src/jula/datamodule/datasets/typo_text_dataset.py
@@ -0,0 +1,48 @@
+import hydra
+import torch
+from torch.utils.data import Dataset
+from transformers import AutoTokenizer, BatchEncoding, PreTrainedTokenizerBase
+from transformers.utils import PaddingStrategy
+
+from jula.utils.constants import TYPO_DUMMY_TOKEN
+
+
+class TypoTextDataset(Dataset):
+    def __init__(
+        self,
+        texts: list[str],
+        model_name_or_path: str = "nlp-waseda/roberta-base-japanese",
+        max_seq_length: int = 512,
+        tokenizer_kwargs: dict = None,
+    ) -> None:
+        self.texts = [text.strip() for text in texts]
+        if tokenizer_kwargs:
+            tokenizer_kwargs = hydra.utils.instantiate(tokenizer_kwargs, _convert_="partial")
+        else:
+            tokenizer_kwargs = {}
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
+            model_name_or_path,
+            **tokenizer_kwargs,
+        )
+        self.max_seq_length = max_seq_length
+
+    def __len__(self) -> int:
+        return len(self.texts)
+
+    def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
+        return self.encode(self.texts[index])
+
+    def encode(self, text: str) -> dict[str, torch.Tensor]:
+        encoding: BatchEncoding = self.tokenizer(
+            text + TYPO_DUMMY_TOKEN,
+            truncation=True,
+            padding=PaddingStrategy.MAX_LENGTH,
+            max_length=self.max_seq_length,
+        )
+        input_ids = encoding["input_ids"]
+        attention_mask = encoding["attention_mask"]
+        return {
+            "input_ids": torch.tensor(input_ids, dtype=torch.long),
+            "attention_mask": torch.tensor(attention_mask, dtype=torch.long),
+            "texts": text,
+        }
diff --git a/src/jula/datamodule/datasets/word_dataset.py b/src/jula/datamodule/datasets/word_dataset.py
index b23ba912..2c6e7ad1 100644
--- a/src/jula/datamodule/datasets/word_dataset.py
+++ b/src/jula/datamodule/datasets/word_dataset.py
@@ -1,19 +1,30 @@
+import logging
+from dataclasses import dataclass
+
 import torch
 from rhoknp import Document
 from rhoknp.rel import ExophoraReferent
 from scipy.special import softmax
 from tokenizers import Encoding
+from tqdm import tqdm
 from transformers.utils import PaddingStrategy
 
 from jula.datamodule.datasets.base_dataset import BaseDataset
-from jula.datamodule.examples import CohesionExample, Task
+from jula.datamodule.examples import (
+    BasePhraseFeatureExample,
+    CohesionExample,
+    DependencyExample,
+    DiscourseExample,
+    Task,
+    WordFeatureExample,
+)
 from jula.datamodule.extractors import BridgingExtractor, CoreferenceExtractor, PasExtractor
 from jula.datamodule.extractors.base import Phrase
-from jula.utils.utils import (
+from jula.utils.constants import (
     BASE_PHRASE_FEATURES,
     CONJFORM_TYPES,
     CONJTYPE_TYPES,
-    DEPENDENCY_TYPES,
+    DEPENDENCY_TYPE2INDEX,
     DISCOURSE_RELATIONS,
     IGNORE_INDEX,
     POS_TYPES,
@@ -21,6 +32,21 @@
     WORD_FEATURES,
 )
 
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class WordExampleSet:
+    example_id: int
+    doc_id: str
+    text: str  # space-delimited word sequence
+    encoding: Encoding
+    word_feature_example: WordFeatureExample
+    base_phrase_feature_example: BasePhraseFeatureExample
+    dependency_example: DependencyExample
+    cohesion_example: CohesionExample
+    discourse_example: DiscourseExample
+
 
 class WordDataset(BaseDataset):
     def __init__(self, *args, **kwargs):
@@ -30,7 +56,7 @@ def __init__(self, *args, **kwargs):
         self.special_tokens: list[str] = [str(e) for e in self.exophora_referents] + [
             "[NULL]",
             "[NA]",
-            "[ROOT]",
+            "[ROOT]",  # TODO: mask in cohesion analysis
         ]
         tokenizer_kwargs = {
             "additional_special_tokens": self.special_tokens,
@@ -43,7 +69,6 @@ def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
         self.cohesion_tasks: list[Task] = [Task(t) for t in kwargs["cohesion_tasks"]]
-
         self.cases: list[str] = list(kwargs["cases"])
         self.pas_targets: list[str] = ["pred", "noun"]
         self.bar_rels = list(kwargs["bar_rels"])
@@ -55,11 +80,17 @@ def __init__(self, *args, **kwargs):
         self.special_to_index: dict[str, int] = {
             token: self.max_seq_length - len(self.special_tokens) + i for i, token in enumerate(self.special_tokens)
         }
-        self.cohesion_examples: dict[str, CohesionExample] = {}
-        for example_id, document in enumerate(self.documents):
-            cohesion_example = self._load_cohesion_example(document)
-            cohesion_example.example_id = example_id
-            self.cohesion_examples[document.doc_id] = cohesion_example
+        self.examples: list[WordExampleSet] = self._load_examples(self.documents)
+        self.special_encoding: Encoding = self.tokenizer(
+            self.special_tokens,
+            is_split_into_words=True,
+            padding=PaddingStrategy.DO_NOT_PAD,
+            truncation=False,
+            add_special_tokens=False,
+        ).encodings[0]
+
+    def __len__(self) -> int:
+        return len(self.examples)
 
     @property
     def special_indices(self) -> list[int]:
@@ -69,94 +100,115 @@ def special_indices(self) -> list[int]:
     def num_special_tokens(self) -> int:
         return len(self.special_tokens)
 
-    def _load_cohesion_example(self, document: Document) -> CohesionExample:
-        example = CohesionExample()
-        example.load(document, tasks=self.cohesion_tasks, extractors=self.extractors)
-        return example
+    def _load_examples(self, documents: list[Document]) -> list[WordExampleSet]:
+        examples = []
+        idx = 0
+        for document in tqdm(documents, dynamic_ncols=True):
+            words = [morpheme.text for morpheme in document.morphemes]
+            encoding: Encoding = self.tokenizer(
+                words,
+                is_split_into_words=True,
+                padding=PaddingStrategy.MAX_LENGTH,
+                truncation=False,
+                max_length=self.max_seq_length - self.num_special_tokens,
+            ).encodings[0]
+            if len(encoding.ids) > self.max_seq_length - self.num_special_tokens:
+                continue
+
+            word_feature_example = WordFeatureExample()
+            word_feature_example.load(document)
+
+            base_phrase_feature_example = BasePhraseFeatureExample()
+            base_phrase_feature_example.load(document)
+
+            dependency_example = DependencyExample()
+            dependency_example.load(document)
+
+            cohesion_example = CohesionExample()
+            cohesion_example.load(document, tasks=self.cohesion_tasks, extractors=self.extractors)
+
+            discourse_example = DiscourseExample()
+            discourse_example.load(document, has_annotation=False)
+            if self.path.name == "train":
+                path = self.path / "disc_crowd" / f"{document.doc_id}.knp"
+            else:
+                path = self.path / "disc_expert" / f"{document.doc_id}.knp"
+            if path.exists():
+                try:
+                    document_disc = Document.from_knp(path.read_text())
+                    if document == document_disc:
+                        discourse_example.load(document_disc)
+                except AssertionError:
+                    logger.warning(f"{path} is not a valid KNP file")
+
+            examples.append(
+                WordExampleSet(
+                    example_id=idx,
+                    doc_id=document.doc_id,
+                    text=" ".join(words),
+                    encoding=encoding,
+                    word_feature_example=word_feature_example,
+                    base_phrase_feature_example=base_phrase_feature_example,
+                    dependency_example=dependency_example,
+                    cohesion_example=cohesion_example,
+                    discourse_example=discourse_example,
+                )
+            )
+            idx += 1
+
+        if len(examples) == 0:
+            logger.error(
+                "No examples to process. "
+                f"Make sure there exist any documents in {self.path} and they are not too long."
+            )
+        return examples
 
     def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
-        document = self.documents[index]
-        cohesion_example = self.cohesion_examples[document.doc_id]
-        return self.encode(document, cohesion_example)
-
-    def encode(self, document: Document, cohesion_example: CohesionExample) -> dict[str, torch.Tensor]:
-        # TODO: deal with the case that the document is too long
-        encoding: Encoding = self.tokenizer(
-            " ".join(morpheme.text for morpheme in document.morphemes),
-            truncation=True,
-            padding=PaddingStrategy.MAX_LENGTH,
-            max_length=self.max_seq_length - self.num_special_tokens,
-        ).encodings[0]
+        return self.encode(self.examples[index])
 
+    def encode(self, example: WordExampleSet) -> dict[str, torch.Tensor]:
         # NOTE: hereafter, indices are given at the word level
-        mrph_types: list[list[int]] = [[IGNORE_INDEX] * 4 for _ in range(self.max_seq_length)]
-        for morpheme in document.morphemes:
-            if morpheme.pos in POS_TYPES:
-                mrph_types[morpheme.global_index][0] = POS_TYPES.index(morpheme.pos)
-            if morpheme.subpos in SUBPOS_TYPES:
-                mrph_types[morpheme.global_index][1] = SUBPOS_TYPES.index(morpheme.subpos)
-            if morpheme.conjtype in CONJTYPE_TYPES:
-                mrph_types[morpheme.global_index][2] = CONJTYPE_TYPES.index(morpheme.conjtype)
-            if morpheme.conjtype in CONJTYPE_TYPES:
-                mrph_types[morpheme.global_index][3] = CONJFORM_TYPES.index(morpheme.conjform)
-
-        word_features = [
-            [0] * len(WORD_FEATURES) if i < len(document.morphemes) else [IGNORE_INDEX] * len(WORD_FEATURES)
-            for i in range(self.max_seq_length)
-        ]
-        for base_phrase in document.base_phrases:
-            head = base_phrase.head
-            end = base_phrase.morphemes[-1]
-            word_features[head.global_index][WORD_FEATURES.index("基本句-主辞")] = 1
-            word_features[end.global_index][WORD_FEATURES.index("基本句-区切")] = 1
-        for phrase in document.phrases:
-            end = phrase.morphemes[-1]
-            word_features[end.global_index][WORD_FEATURES.index("文節-区切")] = 1
 
+        # morpheme type tagging
+        word_feature_example = example.word_feature_example
+        morpheme_type_set = (POS_TYPES, SUBPOS_TYPES, CONJTYPE_TYPES, CONJFORM_TYPES)
+        morpheme_types = [[IGNORE_INDEX] * len(morpheme_type_set) for _ in range(self.max_seq_length)]
+        for morpheme_index, mrph_types in enumerate(word_feature_example.types):
+            for i, (mrph_type, all_types) in enumerate(zip(mrph_types, morpheme_type_set)):
+                if mrph_type in all_types:
+                    morpheme_types[morpheme_index][i] = all_types.index(mrph_type)
+
+        # word feature tagging
+        word_features = [[IGNORE_INDEX] * len(WORD_FEATURES) for _ in range(self.max_seq_length)]
+        for morpheme_index, feature_set in enumerate(word_feature_example.features):
+            for i, word_feature in enumerate(WORD_FEATURES):
+                word_features[morpheme_index][i] = int(word_feature in feature_set)
+
+        # base phrase feature tagging
+        base_phrase_feature_example = example.base_phrase_feature_example
         base_phrase_features = [[IGNORE_INDEX] * len(BASE_PHRASE_FEATURES) for _ in range(self.max_seq_length)]
-        for base_phrase in document.base_phrases:
+        for head_index, feature_set in zip(base_phrase_feature_example.heads, base_phrase_feature_example.features):
             for i, base_phrase_feature in enumerate(BASE_PHRASE_FEATURES):
-                if ":" in base_phrase_feature:
-                    key, value = base_phrase_feature.split(":")
-                else:
-                    key, value = base_phrase_feature, ""
-                head = base_phrase.head
-                if base_phrase.features.get(key, False) in (value, True):
-                    base_phrase_features[head.global_index][i] = 1
-                else:
-                    base_phrase_features[head.global_index][i] = 0
-
-        dependencies = [IGNORE_INDEX for _ in range(self.max_seq_length)]
-        for morpheme in document.morphemes:
-            parent = morpheme.parent
-            if parent:
-                dependencies[morpheme.global_index] = morpheme.parent.global_index
-            else:  # 係り先がなければ文末の[ROOT]を指す
-                dependencies[morpheme.global_index] = self.special_to_index["[ROOT]"]
-
-        intra_mask = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
-        for sentence in document.sentences:
-            morpheme_global_indices = [morpheme.global_index for morpheme in sentence.morphemes]
-            begin, end = map(lambda x: x(morpheme_global_indices), [min, max])
-            for i in range(begin, end + 1):
-                # 末尾の基本句主辞だけ[ROOT]を指す (複数の係り先が[ROOT]を指さないように)
-                if i == sentence.base_phrases[-1].head.global_index:
-                    intra_mask[i][-1] = True
-                else:
-                    for j in range(begin, end + 1):
-                        if i != j:
-                            intra_mask[i][j] = True
-
-        dependency_types = [IGNORE_INDEX for _ in range(self.max_seq_length)]
-        for base_phrase in document.base_phrases:
-            for morpheme in base_phrase.morphemes:
-                if base_phrase.head == morpheme:
-                    dependency_types[morpheme.global_index] = DEPENDENCY_TYPES.index(base_phrase.dep_type.value)
-                else:
-                    dependency_types[morpheme.global_index] = DEPENDENCY_TYPES.index("D")
+                base_phrase_features[head_index][i] = int(base_phrase_feature in feature_set)
+
+        # dependency parsing
+        dependency_example = example.dependency_example
+        dependencies: list[int] = [IGNORE_INDEX for _ in range(self.max_seq_length)]
+        for global_morpheme_index, dependency in enumerate(dependency_example.dependencies):
+            dependencies[global_morpheme_index] = dependency if dependency != -1 else self.special_to_index["[ROOT]"]
+
+        dependency_mask: list[list[bool]] = []  # False -> mask, True -> keep
+        for cands in dependency_example.candidates:
+            cands.append(self.special_to_index["[ROOT]"])
+            dependency_mask.append([(x in cands) for x in range(self.max_seq_length)])
+        dependency_mask += [[False] * self.max_seq_length] * (self.max_seq_length - len(dependency_mask))  # pad
+
+        dependency_types: list[int] = [IGNORE_INDEX for _ in range(self.max_seq_length)]
+        for global_morpheme_index, dependency_type in enumerate(dependency_example.dependency_types):
+            dependency_types[global_morpheme_index] = DEPENDENCY_TYPE2INDEX[dependency_type]
 
         # PAS analysis & coreference resolution
-        cohesion_example.encoding = encoding
+        cohesion_example = example.cohesion_example
         cohesion_target: list[list[list[int]]] = []  # (task, src, tgt)
         candidates_set: list[list[list[int]]] = []  # (task, src, tgt)
         if Task.PAS_ANALYSIS in self.cohesion_tasks:
@@ -176,40 +228,36 @@ def encode(self, document: Document, cohesion_example: CohesionExample) -> dict[
             ret = self._convert_annotation_to_feature(annotation, phrases)
             cohesion_target.append(ret[0])
             candidates_set.append(ret[1])
-
-        special_encoding: Encoding = self.tokenizer(
-            self.special_tokens,
-            is_split_into_words=True,
-            padding=PaddingStrategy.DO_NOT_PAD,
-            truncation=False,
-            add_special_tokens=False,
-        ).encodings[0]
-        merged_encoding: Encoding = Encoding.merge([encoding, special_encoding])
         cohesion_mask = [
             [[(x in cands) for x in range(self.max_seq_length)] for cands in candidates]
             for candidates in candidates_set
         ]  # False -> mask, True -> keep
 
-        # TODO: discourse relation analysis
-        discourse_relations = [
-            [[DISCOURSE_RELATIONS.index("談話関係なし")] * len(DISCOURSE_RELATIONS) for _ in range(self.max_seq_length)]
-            for _ in range(self.max_seq_length)
-        ]
+        discourse_example = example.discourse_example
+        discourse_relations = [[IGNORE_INDEX for _ in range(self.max_seq_length)] for _ in range(self.max_seq_length)]
+        for global_morpheme_index_i, relations in enumerate(discourse_example.discourse_relations):
+            for global_morpheme_index_j, relation in enumerate(relations):
+                if relation in DISCOURSE_RELATIONS:
+                    relation_index = DISCOURSE_RELATIONS.index(relation)
+                    discourse_relations[global_morpheme_index_i][global_morpheme_index_j] = relation_index
+
+        merged_encoding: Encoding = Encoding.merge([example.encoding, self.special_encoding])
 
         return {
-            "example_ids": torch.tensor(cohesion_example.example_id, dtype=torch.long),
+            "example_ids": torch.tensor(example.example_id, dtype=torch.long),
             "input_ids": torch.tensor(merged_encoding.ids, dtype=torch.long),
             "attention_mask": torch.tensor(merged_encoding.attention_mask, dtype=torch.long),
-            "subword_map": torch.tensor(self._gen_subword_map(encoding), dtype=torch.bool),
-            "mrph_types": torch.tensor(mrph_types, dtype=torch.long),
+            "subword_map": torch.tensor(self._gen_subword_map(merged_encoding), dtype=torch.bool),
+            "mrph_types": torch.tensor(morpheme_types, dtype=torch.long),
             "word_features": torch.tensor(word_features, dtype=torch.long),
             "base_phrase_features": torch.tensor(base_phrase_features, dtype=torch.long),
             "dependencies": torch.tensor(dependencies, dtype=torch.long),
-            "intra_mask": torch.tensor(intra_mask, dtype=torch.bool),
+            "intra_mask": torch.tensor(dependency_mask, dtype=torch.bool),
             "dependency_types": torch.tensor(dependency_types, dtype=torch.long),
             "discourse_relations": torch.tensor(discourse_relations, dtype=torch.long),
             "cohesion_target": torch.tensor(cohesion_target, dtype=torch.int),
             "cohesion_mask": torch.tensor(cohesion_mask, dtype=torch.bool),
+            "texts": example.text,
         }
 
     def dump_prediction(
@@ -222,20 +270,20 @@ def dump_prediction(
         task_idx = 0
         if Task.PAS_ANALYSIS in self.cohesion_tasks:
             for _ in self.cases:
-                for i, p in enumerate(self._token2bp_level(result[task_idx], example.phrases[Task.PAS_ANALYSIS])):
+                for i, p in enumerate(self._word2bp_level(result[task_idx], example.phrases[Task.PAS_ANALYSIS])):
                     ret[i].append(p)
                 task_idx += 1
         if Task.BRIDGING in self.cohesion_tasks:
-            for i, p in enumerate(self._token2bp_level(result[task_idx], example.phrases[Task.BRIDGING])):
+            for i, p in enumerate(self._word2bp_level(result[task_idx], example.phrases[Task.BRIDGING])):
                 ret[i].append(p)
             task_idx += 1
         if Task.COREFERENCE in self.cohesion_tasks:
-            for i, p in enumerate(self._token2bp_level(result[task_idx], example.phrases[Task.COREFERENCE])):
+            for i, p in enumerate(self._word2bp_level(result[task_idx], example.phrases[Task.COREFERENCE])):
                 ret[i].append(p)
             task_idx += 1
         return ret
 
-    def _token2bp_level(
+    def _word2bp_level(
         self,
         word_level_output: list[list[float]],
         phrases: list[Phrase],
@@ -259,6 +307,8 @@ def _gen_subword_map(self, encoding: Encoding) -> list[list[bool]]:
         for token_id, word_id in enumerate(encoding.word_ids):
             if word_id is not None:
                 subword_map[word_id][token_id] = True
+        for special_index in self.special_indices:
+            subword_map[special_index][special_index] = True
         return subword_map
 
     def _convert_annotation_to_feature(
@@ -272,19 +322,20 @@ def _convert_annotation_to_feature(
         for phrase in phrases:
             arguments: list[str] = annotation[phrase.dtid]
             candidates: list[int] = phrase.candidates  # phrase level
-            for mrph in filter(lambda m: m.is_target, phrase.children):
+            for mrph in phrase.children:
                 scores: list[int] = [0] * self.max_seq_length
-                for arg_string in arguments:
-                    # arg_string: 著者, 8%C, 15%O, 2, NULL, ...
-                    if arg_string[-2:] in ("%C", "%N", "%O"):
-                        # PAS only
-                        # flag = arg_string[-1]
-                        arg_string = arg_string[:-2]
-                    if arg_string in self.special_to_index:
-                        word_index = self.special_to_index[arg_string]
-                    else:
-                        word_index = phrases[int(arg_string)].dmid
-                    scores[word_index] = 1
+                if mrph.is_target:
+                    for arg_string in arguments:
+                        # arg_string: 著者, 8%C, 15%O, 2, [NULL], ...
+                        if arg_string[-2:] in ("%C", "%N", "%O"):
+                            # PAS only
+                            # flag = arg_string[-1]
+                            arg_string = arg_string[:-2]
+                        if arg_string in self.special_to_index:
+                            word_index = self.special_to_index[arg_string]
+                        else:
+                            word_index = phrases[int(arg_string)].dmid
+                        scores[word_index] = 1
 
                 word_level_candidates: list[int] = []
                 for candidate in candidates:
diff --git a/src/jula/datamodule/examples/__init__.py b/src/jula/datamodule/examples/__init__.py
index df6e6338..525d2123 100644
--- a/src/jula/datamodule/examples/__init__.py
+++ b/src/jula/datamodule/examples/__init__.py
@@ -1,3 +1,14 @@
+from .base_phrase_feature import BasePhraseFeatureExample
 from .cohesion import CohesionExample, Task
+from .dependency import DependencyExample
+from .discourse import DiscourseExample
+from .word_feature import WordFeatureExample
 
-__all__ = ["CohesionExample", "Task"]
+__all__ = [
+    "WordFeatureExample",
+    "BasePhraseFeatureExample",
+    "DependencyExample",
+    "CohesionExample",
+    "DiscourseExample",
+    "Task",
+]
diff --git a/src/jula/datamodule/examples/base_phrase_feature.py b/src/jula/datamodule/examples/base_phrase_feature.py
new file mode 100644
index 00000000..4949ebeb
--- /dev/null
+++ b/src/jula/datamodule/examples/base_phrase_feature.py
@@ -0,0 +1,20 @@
+from rhoknp import Document
+
+from jula.utils.constants import BASE_PHRASE_FEATURES
+
+
+class BasePhraseFeatureExample:
+    """A single training/test example for base phrase feature prediction."""
+
+    def __init__(self) -> None:
+        self.doc_id: str = ""
+        self.heads: list[int] = []  # 基本句単位主辞形態素インデックス
+        self.features: list[set[str]] = []  # 基本句単位素性リスト
+
+    def load(self, document: Document) -> None:
+        self.doc_id = document.doc_id
+        target_feature_set = set(BASE_PHRASE_FEATURES)
+        for base_phrase in document.base_phrases:
+            self.heads.append(base_phrase.head.global_index)
+            features = {k + (f":{v}" if isinstance(v, str) else "") for k, v in base_phrase.features.items()}
+            self.features.append(features & target_feature_set)
diff --git a/src/jula/datamodule/examples/cohesion.py b/src/jula/datamodule/examples/cohesion.py
index 6a68f955..e0d0c987 100644
--- a/src/jula/datamodule/examples/cohesion.py
+++ b/src/jula/datamodule/examples/cohesion.py
@@ -1,14 +1,12 @@
 import logging
 from enum import Enum
-from typing import Optional
 
 from rhoknp import Document
-from tokenizers import Encoding
 
 from jula.datamodule.extractors import Annotation, BridgingExtractor, CoreferenceExtractor, PasExtractor
 from jula.datamodule.extractors.base import Extractor, Mrph, Phrase
 
-logger = logging.getLogger(__file__)
+logger = logging.getLogger(__name__)
 
 
 class Task(Enum):
@@ -25,14 +23,12 @@ class Task(Enum):
 
 
 class CohesionExample:
-    """A single training/test example for bridging anaphora resolution."""
+    """A single training/test example for cohesion analysis."""
 
     def __init__(self) -> None:
-        self.example_id: int = -1
         self.doc_id: str = ""
         self.annotations: dict[Task, Annotation] = {}
         self.phrases: dict[Task, list[Phrase]] = {}
-        self.encoding: Optional[Encoding] = None
 
     @property
     def mrphs(self) -> dict[Task, list[Mrph]]:
diff --git a/src/jula/datamodule/examples/dependency.py b/src/jula/datamodule/examples/dependency.py
new file mode 100644
index 00000000..f5b52973
--- /dev/null
+++ b/src/jula/datamodule/examples/dependency.py
@@ -0,0 +1,30 @@
+from rhoknp import Document
+from rhoknp.units.utils import DepType
+
+
+class DependencyExample:
+    """A single training/test example for dependency parsing."""
+
+    def __init__(self) -> None:
+        self.doc_id: str = ""
+        self.dependencies: list[int] = []  # 形態素単位係り受け
+        self.candidates: list[list[int]] = []  # 形態素単位係り先選択候補
+        self.dependency_types: list[DepType] = []  # 形態素単位係り受けラベル
+
+    def load(self, document: Document) -> None:
+        self.doc_id = document.doc_id
+        for morpheme in document.morphemes:
+            parent = morpheme.parent
+            if parent is not None:
+                self.dependencies.append(parent.global_index)
+            else:  # 係り先がなければ[ROOT]を指す
+                self.dependencies.append(-1)
+
+        for sentence in document.sentences:
+            intra_morphemes = sentence.morphemes
+            for morpheme in sentence.morphemes:
+                self.candidates.append([m.global_index for m in intra_morphemes if m != morpheme])
+
+        self.dependency_types = [DepType.DEPENDENCY] * len(document.morphemes)
+        for base_phrase in document.base_phrases:
+            self.dependency_types[base_phrase.head.global_index] = base_phrase.dep_type
diff --git a/src/jula/datamodule/examples/discourse.py b/src/jula/datamodule/examples/discourse.py
new file mode 100644
index 00000000..f18dfc38
--- /dev/null
+++ b/src/jula/datamodule/examples/discourse.py
@@ -0,0 +1,27 @@
+from rhoknp import Document
+
+from jula.utils.constants import DISCOURSE_RELATION_MAP
+
+
+class DiscourseExample:
+    """A single training/test example for discourse parsing."""
+
+    def __init__(self) -> None:
+        self.doc_id: str = ""
+        self.discourse_relations: list[list[str]] = []
+
+    def load(self, document: Document, has_annotation: bool = True) -> None:
+        self.doc_id = document.doc_id
+        self.discourse_relations = [[""] * len(document.morphemes) for _ in range(len(document.morphemes))]
+        if not has_annotation:
+            return
+        for modifier in document.clauses:
+            modifier_morpheme_id = modifier.end.morphemes[0].global_index
+            for head in document.clauses:
+                head_morpheme_id = head.end.morphemes[0].global_index
+                for discourse_relation in modifier.discourse_relations:
+                    if discourse_relation.head == head:
+                        label = DISCOURSE_RELATION_MAP[discourse_relation.label]
+                        self.discourse_relations[modifier_morpheme_id][head_morpheme_id] = label
+                if not self.discourse_relations[modifier_morpheme_id][head_morpheme_id]:
+                    self.discourse_relations[modifier_morpheme_id][head_morpheme_id] = "談話関係なし"
diff --git a/src/jula/datamodule/examples/word_feature.py b/src/jula/datamodule/examples/word_feature.py
new file mode 100644
index 00000000..a08c0bed
--- /dev/null
+++ b/src/jula/datamodule/examples/word_feature.py
@@ -0,0 +1,21 @@
+from rhoknp import Document
+
+
+class WordFeatureExample:
+    """A single training/test example for word feature prediction."""
+
+    def __init__(self) -> None:
+        self.doc_id: str = ""
+        self.types: list[tuple[str, str, str, str]] = []  # 形態素タイプ (品詞, 品詞細分類, 活用型, 活用形)
+        self.features: list[set[str]] = []  # 形態素素性
+
+    def load(self, document: Document) -> None:
+        self.doc_id = document.doc_id
+        for morpheme in document.morphemes:
+            self.types.append((morpheme.pos, morpheme.subpos, morpheme.conjtype, morpheme.conjform))
+        self.features = [set() for _ in document.morphemes]
+        for base_phrase in document.base_phrases:
+            self.features[base_phrase.head.global_index].add("基本句-主辞")
+            self.features[base_phrase.morphemes[-1].global_index].add("基本句-区切")
+        for phrase in document.phrases:
+            self.features[phrase.morphemes[-1].global_index].add("文節-区切")
diff --git a/src/jula/datamodule/extractors/bridging.py b/src/jula/datamodule/extractors/bridging.py
index 0578066d..9ebeb505 100644
--- a/src/jula/datamodule/extractors/bridging.py
+++ b/src/jula/datamodule/extractors/bridging.py
@@ -7,7 +7,7 @@
 
 from .base import Extractor, Phrase
 
-logger = logging.getLogger(__file__)
+logger = logging.getLogger(__name__)
 
 
 @dataclass(frozen=True)
@@ -58,7 +58,7 @@ def _get_args(
         case: {dmid}%N
         zero: {dmid}%O
         exophor: {exophor}
-        no arg: NULL
+        no arg: [NULL]
         """
         # filter out non-target exophors
         args: list[BaseArgument] = []
@@ -67,7 +67,7 @@ def _get_args(
                 arg.exophora_referent = self._relax_exophora_referent(arg.exophora_referent)
                 if arg.exophora_referent in self.exophora_referents:
                     args.append(arg)
-                elif arg.exophora_referent == ExophoraReferent("[不明]"):
+                elif arg.exophora_referent.text == "[不明]":
                     return []  # don't train uncertain argument
             else:
                 args.append(arg)
@@ -91,4 +91,4 @@ def is_target(self, bp: BasePhrase) -> bool:
 
     @staticmethod
     def is_bridging_target(bp: BasePhrase) -> bool:
-        return bp.features.get("体言") is True and bp.features.get("非用言格解析") is not True
+        return bp.features.get("体言") is True and "非用言格解析" not in bp.features
diff --git a/src/jula/datamodule/extractors/coreference.py b/src/jula/datamodule/extractors/coreference.py
index 5e9366bc..ab3c9d16 100644
--- a/src/jula/datamodule/extractors/coreference.py
+++ b/src/jula/datamodule/extractors/coreference.py
@@ -6,7 +6,7 @@
 
 from .base import Extractor, Phrase
 
-logger = logging.getLogger(__file__)
+logger = logging.getLogger(__name__)
 
 
 @dataclass(frozen=True)
@@ -28,18 +28,18 @@ def extract(
         phrases: list[Phrase],
     ) -> CoreferenceAnnotation:
         bp_list = document.base_phrases
-        arguments_set: list[list[str]] = [[] for _ in bp_list]
+        mentions_set: list[list[str]] = [[] for _ in bp_list]
         for sentence in document.sentences:
-            for anaphor in sentence.base_phrases:
-                is_target_phrase: bool = self.is_target(anaphor) and self._kc_skip_sentence(sentence, document) is False
-                phrases[anaphor.global_index].is_target = is_target_phrase
+            for mention in sentence.base_phrases:
+                is_target_phrase: bool = self.is_target(mention) and self._kc_skip_sentence(sentence, document) is False
+                phrases[mention.global_index].is_target = is_target_phrase
                 if is_target_phrase is False:
                     continue
-                candidates: list[int] = [bp.global_index for bp in bp_list if self.is_candidate(bp, anaphor) is True]
-                phrases[anaphor.global_index].candidates = candidates
-                arguments_set[anaphor.global_index] = self._get_mentions(anaphor, candidates)
+                candidates: list[int] = [bp.global_index for bp in bp_list if self.is_candidate(bp, mention) is True]
+                phrases[mention.global_index].candidates = candidates
+                mentions_set[mention.global_index] = self._get_mentions(mention, candidates)
 
-        return CoreferenceAnnotation(arguments_set)
+        return CoreferenceAnnotation(mentions_set)
 
     def _get_mentions(
         self,
diff --git a/src/jula/datamodule/extractors/pas.py b/src/jula/datamodule/extractors/pas.py
index 9967db6d..60024b75 100644
--- a/src/jula/datamodule/extractors/pas.py
+++ b/src/jula/datamodule/extractors/pas.py
@@ -8,7 +8,7 @@
 
 from .base import Extractor, Phrase
 
-logger = logging.getLogger(__file__)
+logger = logging.getLogger(__name__)
 
 
 @dataclass(frozen=True)
@@ -61,7 +61,7 @@ def _get_args(
         case: {dmid}%N
         zero: {dmid}%O
         exophor: {exophor}
-        no arg: NULL
+        no arg: [NULL]
         """
         # filter out non-target exophors
         args: list[BaseArgument] = []
@@ -70,7 +70,7 @@ def _get_args(
                 arg.exophora_referent = self._relax_exophora_referent(arg.exophora_referent)
                 if arg.exophora_referent in self.exophora_referents:
                     args.append(arg)
-                elif arg.exophora_referent == ExophoraReferent("[不明]"):
+                elif arg.exophora_referent.text == "[不明]":
                     return []  # don't train uncertain argument
             else:
                 args.append(arg)
@@ -107,6 +107,6 @@ def is_target(self, bp: BasePhrase) -> bool:
     def is_pas_target(bp: BasePhrase, verbal: bool, nominal: bool) -> bool:
         if verbal and "用言" in bp.features:
             return True
-        if nominal and bp.features.get("非用言格解析") is True:
+        if nominal and "非用言格解析" in bp.features:
             return True
         return False
diff --git a/src/jula/evaluators/cohesion_analysis_metric.py b/src/jula/evaluators/cohesion_analysis_metric.py
index cfa9aa9b..4eb6ca05 100644
--- a/src/jula/evaluators/cohesion_analysis_metric.py
+++ b/src/jula/evaluators/cohesion_analysis_metric.py
@@ -5,7 +5,7 @@
 from jula.datamodule.datasets.word_dataset import WordDataset
 from jula.datamodule.examples import CohesionExample, Task
 from jula.evaluators.cohesion_scorer import Scorer, ScoreResult
-from jula.writer.knp import CohesionKNPWriter
+from jula.writer.cohesion import CohesionKNPWriter
 
 
 class CohesionAnalysisMetric(Metric):
@@ -27,7 +27,7 @@ def update(
         assert len(output) == len(example_ids)
         for out, eid in zip(output, example_ids):
             self.example_ids.append(eid)
-            gold_example: CohesionExample = dataset.cohesion_examples[dataset.document_ids[eid.item()]]
+            gold_example: CohesionExample = dataset.examples[eid.item()].cohesion_example
             # (rel, phrase, 0 or phrase+special)
             preds: list[list[list[float]]] = dataset.dump_prediction(out.tolist(), gold_example)
             self.predictions.append(
diff --git a/src/jula/evaluators/cohesion_scorer.py b/src/jula/evaluators/cohesion_scorer.py
index 80fa01bd..a376e259 100644
--- a/src/jula/evaluators/cohesion_scorer.py
+++ b/src/jula/evaluators/cohesion_scorer.py
@@ -219,10 +219,8 @@ def _evaluate_pas(self) -> pd.DataFrame:
                     args_pred = predicate_pred.pas.get_arguments(case, relax=False)
                 else:
                     args_pred = []
-                assert len(args_pred) in (
-                    0,
-                    1,
-                )  # Our analyzer predicts one argument for one predicate
+                # Our analyzer predicts one argument for one predicate
+                assert len(args_pred) in (0, 1)
 
                 if global_index in global_index2predicate_gold:
                     predicate_gold = global_index2predicate_gold[global_index]
diff --git a/src/jula/evaluators/dependency_parsing_metric.py b/src/jula/evaluators/dependency_parsing_metric.py
index 901f7f04..84117b5f 100644
--- a/src/jula/evaluators/dependency_parsing_metric.py
+++ b/src/jula/evaluators/dependency_parsing_metric.py
@@ -1,42 +1,94 @@
+from collections import defaultdict
 from typing import Union
 
 import torch
-from rhoknp import BasePhrase, Document, Morpheme, Sentence
+from rhoknp import BasePhrase, Document, Morpheme
+from rhoknp.units.utils import DepType
 from torchmetrics import Metric
 
-from jula.datamodule.datasets.word_dataset import WordDataset
 from jula.evaluators.my_conll18_ud_eval import main as conll18_ud_eval
-from jula.utils.utils import INDEX2DEPENDENCY_TYPE
+from jula.utils.constants import INDEX2DEPENDENCY_TYPE
+
+
+class DependencyManager:
+    def __init__(self) -> None:
+        self.directed_graph: dict[int, set] = defaultdict(set)
+        self.root = False
+
+    def add_edge(self, src: int, dst: int) -> None:
+        self.directed_graph[src].add(dst)
+
+    def remove_edge(self, src: int, dst: int) -> None:
+        self.directed_graph[src].remove(dst)
+
+    def is_cyclic(self, src: int, visited: set[int], cache: dict[int, bool]) -> bool:
+        if src in cache:
+            return cache[src]
+
+        if src in visited:
+            return True
+        else:
+            visited.add(src)
+
+        ret = False
+        for dst in self.directed_graph[src]:
+            if self.is_cyclic(dst, visited, cache):
+                ret = True
+                break
+
+        cache[src] = ret
+        return ret
+
+    def has_cycle(self) -> bool:
+        visited: set[int] = set()
+        cache: dict[int, bool] = {}
+        for src in list(self.directed_graph.keys()):
+            if self.is_cyclic(src, visited, cache):
+                return True
+        return False
 
 
 class DependencyParsingMetric(Metric):
     def __init__(self) -> None:
         super().__init__()
         self.add_state("example_ids", default=[], dist_reduce_fx="cat")
-        self.add_state("preds", default=[], dist_reduce_fx="cat")
-        self.add_state("type_preds", default=[], dist_reduce_fx="cat")
+        self.add_state("dependency_predictions", default=[], dist_reduce_fx="cat")
+        self.add_state("dependency_type_predictions", default=[], dist_reduce_fx="cat")
 
-    def update(self, example_ids: torch.Tensor, preds: torch.Tensor, type_preds: torch.Tensor) -> None:
+    def update(
+        self,
+        example_ids: torch.Tensor,
+        dependency_predictions: torch.Tensor,
+        dependency_type_predictions: torch.Tensor,
+    ) -> None:
         self.example_ids.append(example_ids)
-        self.preds.append(preds)
-        self.type_preds.append(type_preds)
+        self.dependency_predictions.append(dependency_predictions)
+        self.dependency_type_predictions.append(dependency_type_predictions)
 
-    def compute(self, dataset: WordDataset) -> dict[str, Union[torch.Tensor, float]]:
+    def compute(self, documents: list[Document]) -> dict[str, Union[torch.Tensor, float]]:
         sorted_indices = self.unique(self.example_ids)
-        example_ids, preds, type_preds = map(
+        (example_ids, dependency_predictions, dependency_type_predictions) = map(
             lambda x: x[sorted_indices].tolist(),
-            (self.example_ids, self.preds, self.type_preds),
+            [
+                self.example_ids,
+                self.dependency_predictions,
+                self.dependency_type_predictions,
+            ],
         )
-        documents = [dataset.documents[example_id] for example_id in example_ids]
+        documents = [documents[example_id] for example_id in example_ids]
 
-        base_phrase_based_metrics = conll18_ud_eval(*self._to_base_phrase_based_conll(documents, preds, type_preds))
-        morpheme_based_metrics = conll18_ud_eval(*self._to_morpheme_based_conll(documents, preds, type_preds))
+        base_phrase_based_metrics = conll18_ud_eval(
+            *self._to_base_phrase_based_conll(documents, dependency_predictions, dependency_type_predictions)
+        )
+        morpheme_based_metrics = conll18_ud_eval(
+            *self._to_morpheme_based_conll(documents, dependency_predictions, dependency_type_predictions)
+        )
         metrics = {"base_phrase_" + key: value for key, value in base_phrase_based_metrics.items()}
         metrics.update({"morpheme_" + key: value for key, value in morpheme_based_metrics.items()})
         return metrics
 
     @staticmethod
-    def unique(x, dim=None):
+    def unique(x: torch.Tensor, dim: int = None):
         unique, inverse = torch.unique(x, sorted=True, return_inverse=True, dim=dim)
         perm = torch.arange(inverse.size(0), dtype=inverse.dtype, device=inverse.device)
         inverse, perm = inverse.flip([0]), perm.flip([0])
@@ -45,34 +97,40 @@ def unique(x, dim=None):
     def _to_base_phrase_based_conll(
         self,
         documents: list[Document],
-        preds: list[list[int]],
-        type_preds: list[list[int]],
+        dependency_predictions: torch.Tensor,
+        dependency_type_predictions: torch.Tensor,
     ) -> tuple[list[str], list[str]]:
         gold_lines, system_lines = [], []
-        for document, pred, type_pred in zip(documents, preds, type_preds):
+        for document, dependency_prediction, dependency_type_prediction in zip(
+            documents, dependency_predictions, dependency_type_predictions
+        ):
+            sequence_len = len(dependency_prediction)
+
             for sentence in document.sentences:
-                begin, end, morpheme_global_index2base_phrase_index = self._prefetch(sentence, len(pred))
+                morpheme_global_index2base_phrase_index = {
+                    morpheme.global_index: base_phrase.index + 1
+                    for base_phrase in sentence.base_phrases
+                    for morpheme in base_phrase.morphemes
+                }
+                morpheme_global_index2base_phrase_index[sequence_len - 1] = 0
+                dependency_manager = DependencyManager()
+
                 for base_phrase in sentence.base_phrases:
-                    if base_phrase.parent_index >= 0:
-                        gold_head = base_phrase.parent.index + 1
-                        gold_deprel = base_phrase.dep_type.value
-                    else:
-                        gold_head = 0
-                        gold_deprel = "ROOT"
+                    gold_head = base_phrase.parent_index + 1
+                    gold_deprel = base_phrase.dep_type
                     gold_lines.append(self._to_conll_line(base_phrase, gold_head, gold_deprel))
 
-                    p, tp = map(lambda x: x[base_phrase.head.global_index], [pred, type_pred])
-                    system_head = morpheme_global_index2base_phrase_index[p]
-                    system_deprel = INDEX2DEPENDENCY_TYPE[tp] if system_head > 0 else "ROOT"
-                    if 0 < system_head <= base_phrase.index:
-                        system_head, system_deprel = self._find_another_governor(
-                            base_phrase,
-                            morpheme_global_index2base_phrase_index,
-                            pred,
-                            type_pred,
-                        )
-
+                    # goldの基本句主辞
+                    system_head, system_deprel = self.get_system_predictions(
+                        base_phrase,
+                        dependency_prediction[base_phrase.head.global_index],
+                        dependency_type_prediction[base_phrase.head.global_index],
+                        morpheme_global_index2base_phrase_index,
+                        dependency_manager,
+                    )
                     system_lines.append(self._to_conll_line(base_phrase, system_head, system_deprel))
+                    if system_head == 0:
+                        dependency_manager.root = True
 
                 gold_lines.append("\n")
                 system_lines.append("\n")
@@ -81,52 +139,48 @@ def _to_base_phrase_based_conll(
     def _to_morpheme_based_conll(
         self,
         documents: list[Document],
-        preds: list[list[int]],
-        type_preds: list[list[int]],
+        dependency_predictions: torch.Tensor,
+        dependency_type_predictions: torch.Tensor,
     ) -> tuple[list[str], list[str]]:
         gold_lines, system_lines = [], []
-        for document, pred, type_pred in zip(documents, preds, type_preds):
+        for document, dependency_prediction, dependency_type_prediction in zip(
+            documents, dependency_predictions, dependency_type_predictions
+        ):
+            sequence_len = len(dependency_prediction)
+
             for sentence in document.sentences:
-                begin, end, morpheme_global_index2base_phrase_index = self._prefetch(sentence, len(pred))
+                morpheme_global_index2morpheme_index = {
+                    morpheme.global_index: morpheme.index + 1 for morpheme in sentence.morphemes
+                }
+                morpheme_global_index2morpheme_index[sequence_len - 1] = 0
+                dependency_manager = DependencyManager()
+
                 for morpheme in sentence.morphemes:
-                    if morpheme.parent:
-                        gold_head = morpheme.parent.index + 1
-                        if morpheme == morpheme.base_phrase.head:
-                            gold_deprel = morpheme.base_phrase.dep_type.value
-                        else:
-                            gold_deprel = "D"
+                    gold_head = morpheme.parent.index + 1 if morpheme.parent else 0
+                    if morpheme == morpheme.base_phrase.head:
+                        gold_deprel = morpheme.base_phrase.dep_type
                     else:
-                        gold_head = 0
-                        gold_deprel = "ROOT"
+                        gold_deprel = DepType.DEPENDENCY
                     gold_lines.append(self._to_conll_line(morpheme, gold_head, gold_deprel))
 
-                    p, tp = map(lambda x: x[morpheme.global_index], [pred, type_pred])
-                    if begin <= p <= end:
-                        system_head = p - begin + 1
-                        system_deprel = INDEX2DEPENDENCY_TYPE[tp]
-                    else:
-                        system_head = 0
-                        system_deprel = "ROOT"
+                    system_head, system_deprel = self.get_system_predictions(
+                        morpheme,
+                        dependency_prediction[morpheme.global_index],
+                        dependency_type_prediction[morpheme.global_index],
+                        morpheme_global_index2morpheme_index,
+                        dependency_manager,
+                    )
+
                     system_lines.append(self._to_conll_line(morpheme, system_head, system_deprel))
+                    if system_head == 0:
+                        dependency_manager.root = True
 
                 gold_lines.append("\n")
                 system_lines.append("\n")
         return gold_lines, system_lines
 
     @staticmethod
-    def _prefetch(sentence: Sentence, max_seq_len: int) -> tuple[int, int, dict[int, int]]:
-        morpheme_global_indices = [morpheme.global_index for morpheme in sentence.morphemes]
-        begin, end = map(lambda x: x(morpheme_global_indices), [min, max])
-        morpheme_global_index2base_phrase_index = {
-            morpheme.global_index: base_phrase.index + 1
-            for base_phrase in sentence.base_phrases
-            for morpheme in base_phrase.morphemes
-        }
-        morpheme_global_index2base_phrase_index[max_seq_len - 1] = 0
-        return begin, end, morpheme_global_index2base_phrase_index
-
-    @staticmethod
-    def _to_conll_line(unit: Union[BasePhrase, Morpheme], head: int, deprel: str) -> str:
+    def _to_conll_line(unit: Union[BasePhrase, Morpheme], head: int, deprel: DepType) -> str:
         id_ = unit.index + 1  # 0-origin -> 1-origin
         if isinstance(unit, BasePhrase):
             form = "".join(morpheme.surf for morpheme in unit.morphemes)
@@ -139,19 +193,65 @@ def _to_conll_line(unit: Union[BasePhrase, Morpheme], head: int, deprel: str) ->
         feats = "_"
         deps = "_"
         misc = "_"
-        return "\t".join(map(str, [id_, form, lemma, upos, xpos, feats, head, deprel, deps, misc]))
+        return "\t".join(
+            map(
+                str,
+                [id_, form, lemma, upos, xpos, feats, head, deprel.value, deps, misc],
+            )
+        )
 
     @staticmethod
-    def _find_another_governor(
-        base_phrase: BasePhrase,
-        morpheme_global_index2base_phrase_index: dict[int, int],
-        pred: list[int],
-        type_pred,
-    ) -> tuple[int, str]:
-        for morpheme in base_phrase.morphemes:
-            p, tp = map(lambda x: x[morpheme.global_index], [pred, type_pred])
-            candidate = morpheme_global_index2base_phrase_index[p]
-            if candidate > base_phrase.index:
-                return candidate, INDEX2DEPENDENCY_TYPE[tp]
+    def get_number_of_units(unit: Union[BasePhrase, Morpheme]) -> int:
+        if isinstance(unit, BasePhrase):
+            return len(unit.sentence.base_phrases)
+        elif isinstance(unit, Morpheme):
+            return len(unit.sentence.morphemes)
+        else:
+            raise TypeError
+
+    def get_system_predictions(
+        self,
+        unit: Union[BasePhrase, Morpheme],
+        topk_heads: torch.Tensor,  # (k, )
+        topk_dependency_types: torch.Tensor,  # (k, )
+        morpheme_global_index2unit_index: dict[int, int],
+        dependency_manager: DependencyManager,
+    ) -> tuple[int, DepType]:
+        src = unit.index + 1
+        for head, dependency_type in zip(topk_heads, topk_dependency_types):
+            system_head = morpheme_global_index2unit_index[head]
+            system_deprel = INDEX2DEPENDENCY_TYPE[dependency_type]
+            dependency_manager.add_edge(src, system_head)
+            if dependency_manager.has_cycle() or (system_head == 0 and dependency_manager.root):
+                dependency_manager.remove_edge(src, system_head)
+            else:
+                break
         else:
-            return 0, "ROOT"
+            # 末尾の基本句/形態素まで見てROOTがない時
+            if src == self.get_number_of_units(unit) and not dependency_manager.root:
+                system_head, system_deprel = 0, DepType.DEPENDENCY
+            else:
+                system_head, system_deprel = self.resolve_dependency(unit, dependency_manager)
+        return system_head, system_deprel
+
+    def resolve_dependency(
+        self, unit: Union[BasePhrase, Morpheme], dependency_manager: DependencyManager
+    ) -> tuple[int, DepType]:
+        src = unit.index + 1
+        num_units = self.get_number_of_units(unit)
+        # 日本語の係り受けは基本的にleft-to-rightなので、まず右方向に係れるか調べる
+        for dst in range(src + 1, num_units + 1):
+            dependency_manager.add_edge(src, dst)
+            if dependency_manager.has_cycle():
+                dependency_manager.remove_edge(src, dst)
+            else:
+                return dst, DepType.DEPENDENCY
+
+        for dst in range(src - 1, 0, -1):
+            dependency_manager.add_edge(src, dst)
+            if dependency_manager.has_cycle():
+                dependency_manager.remove_edge(src, dst)
+            else:
+                return dst, DepType.DEPENDENCY
+
+        raise RuntimeError("couldn't resolve dependency")
diff --git a/src/jula/evaluators/discourse_parsing_metric.py b/src/jula/evaluators/discourse_parsing_metric.py
new file mode 100644
index 00000000..cfeffab4
--- /dev/null
+++ b/src/jula/evaluators/discourse_parsing_metric.py
@@ -0,0 +1,54 @@
+from typing import Union
+
+import torch
+from torchmetrics import Metric
+from torchmetrics.functional import f1_score
+
+from jula.utils.constants import DISCOURSE_RELATIONS, IGNORE_INDEX
+
+
+class DiscourseParsingMetric(Metric):
+    full_state_update: bool = False
+
+    def __init__(self):
+        super().__init__()
+        # Metric state variables can either be torch.Tensor or an empty list which can be used to store torch.Tensors`.
+        # i.e. Expected metric state to be either a Tensor or a list of Tensor
+        self.add_state("predictions", default=list(), dist_reduce_fx="cat")  # list[torch.Tensor]
+        self.add_state("labels", default=list(), dist_reduce_fx="cat")  # list[torch.Tensor]
+
+    def update(
+        self,
+        discourse_parsing_predictions: torch.Tensor,  # (b, seq, seq)
+        discourse_parsing_labels: torch.Tensor,  # (b, seq, seq)
+    ) -> None:
+        self.predictions.append(discourse_parsing_predictions)
+        self.labels.append(discourse_parsing_labels)
+
+    @staticmethod
+    def _filter_ignore_index(t: torch.Tensor, labels: torch.Tensor, ignore_index: int = IGNORE_INDEX) -> torch.Tensor:
+        return torch.masked_select(t, labels != ignore_index)
+
+    def compute(self) -> dict[str, Union[torch.Tensor, float]]:
+        predictions = self.predictions.view(-1)
+        labels = self.labels.view(-1)
+
+        predictions = self._filter_ignore_index(predictions, labels)
+        labels = self._filter_ignore_index(labels, labels)
+        if labels.numel() == 0:
+            discourse_parsing_f1 = 0.0
+        else:
+            discourse_parsing_f1 = f1_score(predictions, labels)
+
+        no_relation_index = DISCOURSE_RELATIONS.index("談話関係なし")
+        predictions = self._filter_ignore_index(predictions, labels, ignore_index=no_relation_index)
+        labels = self._filter_ignore_index(labels, labels, ignore_index=no_relation_index)
+        if labels.numel() == 0:
+            discourse_parsing_f1_no_relation_ignored = 0.0
+        else:
+            discourse_parsing_f1_no_relation_ignored = f1_score(predictions, labels)
+
+        return {
+            "discourse_parsing_f1": discourse_parsing_f1,
+            "discourse_parsing_f1_no_relation_ignored": discourse_parsing_f1_no_relation_ignored,
+        }
diff --git a/src/jula/evaluators/my_conll18_ud_eval.py b/src/jula/evaluators/my_conll18_ud_eval.py
index f11314a6..16f74f5d 100644
--- a/src/jula/evaluators/my_conll18_ud_eval.py
+++ b/src/jula/evaluators/my_conll18_ud_eval.py
@@ -92,7 +92,7 @@
 from __future__ import division, print_function
 
 # import argparse  # FIX
-import io
+# import io        # FIX
 import sys
 import unicodedata
 import unittest
@@ -229,16 +229,16 @@ def __init__(self, span, columns, is_multiword):
 
     # Load the CoNLL-U file
     index, sentence_start = 0, None
-    line_count = 0
+    line_count = 0  # FIX
     while True:
-        # line = file.readline()
-        # if not line:
-        #     break
+        # line = file.readline()                 # FIX
+        # if not line:                           # FIX
+        #     break                              # FIX
         if line_count >= len(lines):  # FIX
-            break
+            break  # FIX
         line = lines[line_count].rstrip("\r\n")  # FIX
-        line_count += 1
-        # line = _decode(line.rstrip("\r\n"))
+        line_count += 1  # FIX
+        # line = _decode(line.rstrip("\r\n"))    # FIX
 
         # Handle sentence start boundaries
         if sentence_start is None:
@@ -252,8 +252,7 @@ def __init__(self, span, columns, is_multiword):
             # Add parent and children UDWord links and check there are no cycles
             def process_word(word):
                 if word.parent == "remapping":
-                    # raise UDError("There is a cycle in a sentence")
-                    pass  # FIX
+                    raise UDError("There is a cycle in a sentence")
                 if word.parent is None:
                     head = int(word.columns[HEAD])
                     if head < 0 or head > len(ud.words) - sentence_start:
@@ -274,8 +273,7 @@ def process_word(word):
 
             # Check there is a single root node
             if len([word for word in ud.words[sentence_start:] if word.parent is None]) != 1:
-                # raise UDError("There are multiple roots in a sentence")
-                pass  # FIX
+                raise UDError("There are multiple roots in a sentence")
 
             # End the sentence
             ud.sentences[-1].end = index
@@ -311,9 +309,9 @@ def process_word(word):
                 raise UDError("Cannot parse multi-word token ID '{}'".format(_encode(columns[ID])))
 
             for _ in range(start, end + 1):
-                # word_line = _decode(file.readline().rstrip("\r\n"))
+                # word_line = _decode(file.readline().rstrip("\r\n"))  # FIX
+                word_line = _decode(lines[line_count].rstrip("\r\n"))  # FIX
                 line_count += 1  # FIX
-                word_line = _decode(lines[line_count].rstrip("\r\n"))
                 word_columns = word_line.split("\t")
                 if len(word_columns) != 10:
                     raise UDError(
@@ -671,7 +669,12 @@ def _load_words(words):
                 for part in parts[1:]:
                     num_words += 1
                     lines.append("{}\t{}\t_\t_\t_\t_\t{}\t_\t_\t_".format(num_words, part, int(num_words > 1)))
-        return load_conllu((io.StringIO if sys.version_info >= (3, 0) else io.BytesIO)("\n".join(lines + ["\n"])))
+        return load_conllu(lines + ["\n"])
+        # return load_conllu(
+        #     (io.StringIO if sys.version_info >= (3, 0) else io.BytesIO)(
+        #         "\n".join(lines + ["\n"])
+        #     )
+        # )
 
     def _test_exception(self, gold, system):
         self.assertRaises(UDError, evaluate, self._load_words(gold), self._load_words(system))
diff --git a/src/jula/evaluators/phrase_analysis_metric.py b/src/jula/evaluators/phrase_analysis_metric.py
index 79936016..f3066d2f 100644
--- a/src/jula/evaluators/phrase_analysis_metric.py
+++ b/src/jula/evaluators/phrase_analysis_metric.py
@@ -7,7 +7,7 @@
 from seqeval.scheme import IOB2
 from torchmetrics import Metric
 
-from jula.utils.utils import BASE_PHRASE_FEATURES, IGNORE_INDEX, WORD_FEATURES
+from jula.utils.constants import BASE_PHRASE_FEATURES, IGNORE_INDEX, WORD_FEATURES
 
 
 class PhraseAnalysisMetric(Metric):
@@ -21,11 +21,11 @@ def __init__(self) -> None:
 
     def update(
         self,
-        example_ids: torch.LongTensor,
-        word_feature_predictions: torch.LongTensor,
-        word_features: torch.LongTensor,
-        base_phrase_feature_predictions: torch.LongTensor,
-        base_phrase_features: torch.LongTensor,
+        example_ids: torch.Tensor,
+        word_feature_predictions: torch.Tensor,
+        word_features: torch.Tensor,
+        base_phrase_feature_predictions: torch.Tensor,
+        base_phrase_features: torch.Tensor,
     ):
         self.example_ids.append(example_ids)
         self.word_feature_predictions.append(word_feature_predictions)
@@ -33,9 +33,9 @@ def update(
         self.base_phrase_feature_predictions.append(base_phrase_feature_predictions)
         self.base_phrase_features.append(base_phrase_features)
 
-    def compute(self) -> dict[str, Union[torch.LongTensor, float]]:
+    def compute(self) -> dict[str, Union[torch.Tensor, float]]:
         sorted_indices = self.unique(self.example_ids)
-        # (num_base_phrase_features, b, seq_len)
+        # (num_base_phrase_features, b, seq)
         (word_feature_predictions, word_features, base_phrase_feature_predictions, base_phrase_features,) = map(
             lambda x: x[sorted_indices].permute(2, 0, 1),
             [
@@ -74,13 +74,13 @@ def align_predictions(
 
     def compute_word_feature_metrics(
         self,
-        word_feature_predictions: torch.LongTensor,
-        word_features: torch.LongTensor,
+        word_feature_predictions: torch.Tensor,
+        word_features: torch.Tensor,
     ) -> dict[str, float]:
         word_feature_metrics = {}
         aligned_predictions, aligned_labels = [], []
         for i, (word_feature_prediction, word_feature_label) in enumerate(zip(word_feature_predictions, word_features)):
-            # prediction / label: (b, seq_len)
+            # prediction / label: (b, seq)
             aligned_prediction, aligned_label = self.align_predictions(
                 prediction=word_feature_prediction.detach().cpu().numpy(),
                 label=word_feature_label.detach().cpu().numpy(),
@@ -109,15 +109,15 @@ def compute_word_feature_metrics(
 
     def compute_base_phrase_feature_metrics(
         self,
-        base_phrase_feature_predictions: torch.LongTensor,
-        base_phrase_features: torch.LongTensor,
+        base_phrase_feature_predictions: torch.Tensor,
+        base_phrase_features: torch.Tensor,
     ) -> dict[str, float]:
         base_phrase_feature_metrics = {}
         aligned_predictions, aligned_labels = [], []
         for i, (base_phrase_feature_prediction, base_phrase_feature_label) in enumerate(
             zip(base_phrase_feature_predictions, base_phrase_features)
         ):
-            # pred/label: (batch_size, seq_len)
+            # prediction / label: (b, seq)
             aligned_prediction, aligned_label = self.align_predictions(
                 prediction=base_phrase_feature_prediction.detach().cpu().numpy(),
                 label=base_phrase_feature_label.detach().cpu().numpy(),
diff --git a/src/jula/evaluators/word_analyzer.py b/src/jula/evaluators/word_analyzer.py
index bf2de391..08b19718 100644
--- a/src/jula/evaluators/word_analyzer.py
+++ b/src/jula/evaluators/word_analyzer.py
@@ -4,7 +4,7 @@
 from torchmetrics import Metric
 from torchmetrics.functional import f1_score
 
-from jula.utils.utils import IGNORE_INDEX
+from jula.utils.constants import IGNORE_INDEX
 
 
 class WordAnalysisMetric(Metric):
diff --git a/src/jula/evaluators/word_segmenter.py b/src/jula/evaluators/word_segmenter.py
index 34511c40..f5dc1bd3 100644
--- a/src/jula/evaluators/word_segmenter.py
+++ b/src/jula/evaluators/word_segmenter.py
@@ -5,7 +5,7 @@
 from seqeval.scheme import IOB2
 from torchmetrics import Metric
 
-from jula.utils.utils import IGNORE_INDEX, INDEX2SEG_TYPE
+from jula.utils.constants import IGNORE_INDEX, INDEX2SEG_TYPE
 
 
 class WordSegmenterMetric(Metric):
diff --git a/src/jula/models/char_module.py b/src/jula/models/char_module.py
index 33fbe967..8a0d4cf4 100644
--- a/src/jula/models/char_module.py
+++ b/src/jula/models/char_module.py
@@ -5,7 +5,7 @@
 import torch
 from omegaconf import DictConfig
 from pytorch_lightning.core.lightning import LightningModule
-from transformers import AutoTokenizer, PretrainedConfig, PreTrainedTokenizer
+from transformers import AutoTokenizer, PretrainedConfig, PreTrainedTokenizerBase
 
 from jula.evaluators.word_segmenter import WordSegmenterMetric
 from jula.models.models.char_encoder import CharEncoder
@@ -18,7 +18,7 @@ def __init__(self, hparams: DictConfig) -> None:
         self.hparams.update(hparams)
         self.save_hyperparameters()
 
-        self.tokenizer: PreTrainedTokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             hparams.model.model_name_or_path,
             **hydra.utils.instantiate(hparams.dataset.tokenizer_kwargs, _convert_="partial"),
         )
@@ -27,11 +27,10 @@ def __init__(self, hparams: DictConfig) -> None:
         self.test_corpora = list(hparams.dataset.test.keys())
 
         self.char_encoder: CharEncoder = CharEncoder(hparams, self.tokenizer)
+
         pretrained_model_config: PretrainedConfig = self.char_encoder.pretrained_model.config
-        self.word_segmenter: WordSegmenter = WordSegmenter(
-            hparams=hparams,
-            pretrained_model_config=pretrained_model_config,
-        )
+
+        self.word_segmenter: WordSegmenter = WordSegmenter(hparams, pretrained_model_config)
         self.valid_word_segmenter_metrics: dict[str, WordSegmenterMetric] = {
             corpus: WordSegmenterMetric() for corpus in self.valid_corpora
         }
@@ -42,9 +41,7 @@ def __init__(self, hparams: DictConfig) -> None:
     def forward(self, **kwargs) -> dict[str, dict[str, torch.Tensor]]:
         encoder_output = self.char_encoder(kwargs)  # (b, seq_len, h)
         word_segmenter_outputs = self.word_segmenter(encoder_output, kwargs)
-        return {
-            "word_segmenter_outputs": word_segmenter_outputs,
-        }
+        return {"word_segmenter_outputs": word_segmenter_outputs}
 
     def training_step(self, batch: Any, batch_idx: int) -> dict[str, Any]:
         outputs: dict[str, dict[str, torch.Tensor]] = self(**batch)
@@ -64,10 +61,7 @@ def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[i
             "seg_labels": batch["seg_labels"],
         }
         self.valid_word_segmenter_metrics[corpus].update(**word_segmenter_args)
-        self.log(
-            "valid/word_segmenter_loss",
-            outputs["word_segmenter_outputs"]["loss"],
-        )
+        self.log("valid/word_segmenter_loss", outputs["word_segmenter_outputs"]["loss"])
 
     def validation_epoch_end(self, validation_step_outputs) -> None:
         f1s: list[float] = []
@@ -78,12 +72,8 @@ def validation_epoch_end(self, validation_step_outputs) -> None:
                     word_segmenter_f1 += value
                 self.log(f"valid_{corpus}/{name}", value)
                 metric.reset()
-        self.log(
-            "valid/word_segmenter_f1",
-            word_segmenter_f1 / len(self.valid_word_segmenter_metrics),
-        )
+        self.log("valid/word_segmenter_f1", word_segmenter_f1 / len(self.valid_word_segmenter_metrics))
         f1s.append(word_segmenter_f1 / len(self.valid_word_segmenter_metrics))
-
         self.log("valid/f1", mean(f1s))
 
     def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Any:
@@ -94,10 +84,7 @@ def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] =
             "seg_labels": batch["seg_labels"],
         }
         self.test_word_segmenter_metrics[corpus].update(**word_segmenter_args)
-        self.log(
-            "test/word_segmenter_loss",
-            outputs["word_segmenter_outputs"]["loss"],
-        )
+        self.log("test/word_segmenter_loss", outputs["word_segmenter_outputs"]["loss"])
 
     def test_epoch_end(self, test_step_outputs) -> None:
         f1s: list[float] = []
@@ -108,20 +95,15 @@ def test_epoch_end(self, test_step_outputs) -> None:
                     word_segmenter_f1 += value
                 self.log(f"test_{corpus}/{name}", value)
                 metric.reset()
-        self.log(
-            "test/word_segmenter_f1",
-            word_segmenter_f1 / len(self.test_word_segmenter_metrics),
-        )
+        self.log("test/word_segmenter_f1", word_segmenter_f1 / len(self.test_word_segmenter_metrics))
         f1s.append(word_segmenter_f1 / len(self.test_word_segmenter_metrics))
-
         self.log("test/f1", mean(f1s))
 
     def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Any:
         outputs: dict[str, dict[str, torch.Tensor]] = self(**batch)
         return {
-            "example_ids": batch["example_ids"],
+            "input_ids": batch["input_ids"],
             "word_segmenter_logits": outputs["word_segmenter_outputs"]["logits"],
-            "word_segmenter_labels": batch["seg_labels"],
         }
 
     def configure_optimizers(self):
diff --git a/src/jula/models/models/char_encoder.py b/src/jula/models/models/char_encoder.py
index 3bc5fe8a..bc42196c 100644
--- a/src/jula/models/models/char_encoder.py
+++ b/src/jula/models/models/char_encoder.py
@@ -1,14 +1,14 @@
 import torch
 import torch.nn as nn
 from omegaconf import DictConfig
-from transformers import AutoModel, PreTrainedTokenizer
+from transformers import AutoModel, PreTrainedTokenizerBase
 from transformers.models.roberta.modeling_roberta import create_position_ids_from_input_ids
 
-from jula.utils.utils import ENE_TYPE_BIES
+from jula.utils.constants import ENE_TYPE_BIES
 
 
 class CharEncoder(nn.Module):
-    def __init__(self, hparams: DictConfig, tokenizer: PreTrainedTokenizer) -> None:
+    def __init__(self, hparams: DictConfig, tokenizer: PreTrainedTokenizerBase) -> None:
         super().__init__()
         self.hparams = hparams
 
@@ -16,7 +16,7 @@ def __init__(self, hparams: DictConfig, tokenizer: PreTrainedTokenizer) -> None:
         self.pretrained_model.resize_token_embeddings(len(tokenizer))
         self.word_embed = self.pretrained_model.embeddings.word_embeddings
 
-        self.max_ene_num: int = self.hparams.dataset.max_ene_num
+        self.max_ene_num: int = self.hparams.dataset.get("max_ene_num", 0)
         if self.max_ene_num > 0:
             self.add_ene_embed_before_encoder: bool = hparams.add_ene_embed_before_encoder
             self.ene_embed: nn.Embedding = nn.Embedding(
diff --git a/src/jula/models/models/dependency_parser.py b/src/jula/models/models/dependency_parser.py
index 772d4358..8a31770e 100644
--- a/src/jula/models/models/dependency_parser.py
+++ b/src/jula/models/models/dependency_parser.py
@@ -5,11 +5,11 @@
 import torch.nn as nn
 from transformers import PretrainedConfig
 
-from jula.utils.utils import DEPENDENCY_TYPES, IGNORE_INDEX
+from jula.utils.constants import DEPENDENCY_TYPE2INDEX, IGNORE_INDEX
 
 
 class DependencyParser(nn.Module):
-    def __init__(self, pretrained_model_config: PretrainedConfig) -> None:
+    def __init__(self, pretrained_model_config: PretrainedConfig, k: int) -> None:
         super().__init__()
         hidden_size = pretrained_model_config.hidden_size
         # Dependency Parsing as Head Selection [Zhang+ EACL2017]
@@ -17,7 +17,7 @@ def __init__(self, pretrained_model_config: PretrainedConfig) -> None:
         self.U_a = nn.Linear(hidden_size, hidden_size)
         self.v_a = nn.Linear(hidden_size, 1, bias=False)
 
-        self.head = nn.Sequential(
+        self.dependency_type_head = nn.Sequential(
             OrderedDict(
                 [
                     ("dense", nn.Linear(hidden_size * 2, hidden_size)),
@@ -26,35 +26,44 @@ def __init__(self, pretrained_model_config: PretrainedConfig) -> None:
                         "dropout",
                         nn.Dropout(pretrained_model_config.hidden_dropout_prob),
                     ),
-                    ("fc", nn.Linear(hidden_size, len(DEPENDENCY_TYPES))),
+                    ("fc", nn.Linear(hidden_size, len(DEPENDENCY_TYPE2INDEX))),
                 ]
             )
         )
 
-    def forward(
-        self, pooled_outputs: torch.Tensor, dependencies: Optional[torch.Tensor] = None
-    ):  # (batch_size, max_seq_len, hidden_size)
+        self.k = k
+
+    def forward(self, pooled_outputs: torch.Tensor, dependencies: Optional[torch.Tensor] = None):
+        # (b, seq, h)
         h_i = self.W_a(pooled_outputs)
         h_j = self.U_a(pooled_outputs)
         dependency_logits = self.v_a(torch.tanh(h_i.unsqueeze(1) + h_j.unsqueeze(2))).squeeze(-1)
+
+        pooled_outputs = pooled_outputs.unsqueeze(2)
+        batch_size, sequence_len, k, hidden_size = pooled_outputs.shape
         if dependencies is not None:
-            dependencies = torch.where(
-                dependencies != IGNORE_INDEX,
-                dependencies,
-                torch.zeros_like(dependencies),
-            )
+            # gather_indexにINGORE(=-100)を渡すことはできないので、0に上書き
+            dependencies = (dependencies * dependencies.ne(IGNORE_INDEX)).unsqueeze(2).unsqueeze(3)
         else:
-            dependencies = torch.argmax(dependency_logits, dim=2)
-
-        batch_size, max_seq_len, hidden_size = pooled_outputs.shape
-        # governor_embeddings[0][0][0] == pooled_outputs[0][dependencies[0][0][0]]
-        # governor_embeddings[0][0][1] == pooled_outputs[0][dependencies[0][0][1]]
-        # governor_embeddings[0][1][0] == pooled_outputs[0][dependencies[0][1][0]]
-        # governor_embeddings[1][0][0] == pooled_outputs[1][dependencies[1][0][0]]
-        governor_embeddings = torch.gather(
-            pooled_outputs.unsqueeze(1).expand(batch_size, max_seq_len, max_seq_len, hidden_size),
+            dependencies = torch.topk(dependency_logits, self.k, dim=2).indices.unsqueeze(3)
+            k = self.k
+
+        # head_hidden_states[0][0][0] == pooled_outputs[0][dependencies[0][0][0]]
+        # head_hidden_states[0][1][0] == pooled_outputs[0][dependencies[0][1][0]]
+        # head_hidden_states[1][0][0] == pooled_outputs[1][dependencies[1][0][0]]
+        head_hidden_states = torch.gather(
+            pooled_outputs.expand(batch_size, sequence_len, sequence_len, hidden_size),
             dim=2,
-            index=dependencies.unsqueeze(2).unsqueeze(2).expand(batch_size, max_seq_len, 1, hidden_size),
-        )  # (batch_size, max_seq_len, TopK, hidden_size)
-        dependency_type_logits = self.head(torch.cat([pooled_outputs, governor_embeddings.squeeze(2)], dim=2))
+            index=dependencies.expand(batch_size, sequence_len, k, hidden_size),
+        )
+        # (b, seq, 1 or k, num_dependency_types)
+        dependency_type_logits = self.dependency_type_head(
+            torch.cat(
+                [
+                    pooled_outputs.expand(batch_size, sequence_len, k, hidden_size),
+                    head_hidden_states,
+                ],
+                dim=-1,
+            )
+        )
         return dependency_logits, dependency_type_logits
diff --git a/src/jula/models/models/discourse_parser.py b/src/jula/models/models/discourse_parser.py
index e69de29b..4ebfeb73 100644
--- a/src/jula/models/models/discourse_parser.py
+++ b/src/jula/models/models/discourse_parser.py
@@ -0,0 +1,24 @@
+import torch
+import torch.nn as nn
+from transformers import PretrainedConfig
+
+from jula.utils.constants import DISCOURSE_RELATIONS
+
+
+class DiscourseParser(nn.Module):
+    def __init__(self, pretrained_model_config: PretrainedConfig) -> None:
+        super().__init__()
+        hidden_size = pretrained_model_config.hidden_size
+
+        self.dropout = nn.Dropout(0.0)
+
+        self.l_src = nn.Linear(hidden_size, hidden_size)
+        self.l_tgt = nn.Linear(hidden_size, hidden_size)
+        self.out = nn.Linear(hidden_size, len(DISCOURSE_RELATIONS), bias=False)
+
+    def forward(self, pooled_outputs: torch.Tensor) -> torch.Tensor:
+        batch_size, sequence_len, _ = pooled_outputs.size()
+        h_src = self.l_src(self.dropout(pooled_outputs))  # (b, seq, hid)
+        h_tgt = self.l_tgt(self.dropout(pooled_outputs))  # (b, seq, hid)
+        h = torch.tanh(self.dropout(h_src.unsqueeze(2) + h_tgt.unsqueeze(1)))  # (b, seq, seq, hid)
+        return self.out(h)  # (b, seq, seq, rel)
diff --git a/src/jula/models/models/phrase_analyzer.py b/src/jula/models/models/phrase_analyzer.py
index 64adffd7..58c28d29 100644
--- a/src/jula/models/models/phrase_analyzer.py
+++ b/src/jula/models/models/phrase_analyzer.py
@@ -5,7 +5,7 @@
 import torch.nn.functional as F
 from transformers import PretrainedConfig
 
-from jula.utils.utils import BASE_PHRASE_FEATURES, IGNORE_INDEX, WORD_FEATURES
+from jula.utils.constants import BASE_PHRASE_FEATURES, IGNORE_INDEX, WORD_FEATURES
 
 
 class PhraseAnalyzer(nn.Module):
@@ -45,10 +45,10 @@ def __init__(self, pretrained_model_config: PretrainedConfig) -> None:
 
     def forward(self, pooled_outputs: torch.Tensor, batch: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
         outputs: dict[str, torch.Tensor] = {}
-        # (b, seq_len, num_word_features)
+        # (b, seq, num_word_features)
         word_feature_logits = self.word_feature_head(pooled_outputs)
         outputs["word_feature_logits"] = word_feature_logits
-        # (b, seq_len, num_base_phrase_features)
+        # (b, seq, num_base_phrase_features)
         base_phrase_feature_logits = self.base_phrase_feature_head(pooled_outputs)
         outputs["base_phrase_feature_logits"] = base_phrase_feature_logits
         if "word_features" in batch:
@@ -71,11 +71,11 @@ def forward(self, pooled_outputs: torch.Tensor, batch: dict[str, torch.Tensor])
 
     @staticmethod
     def compute_loss(
-        input_: torch.FloatTensor,
-        target: torch.FloatTensor,
-        num_units: torch.LongTensor,
+        input_: torch.Tensor,
+        target: torch.Tensor,
+        num_units: torch.Tensor,
     ) -> torch.Tensor:
-        # (b, seq_len, num_features)
+        # (b, seq, num_features)
         losses = F.binary_cross_entropy(input=input_, target=target, reduction="none")
         # 各featureのlossの和
         losses = torch.sum(torch.sum(losses, dim=1), dim=1)  # (b, )
diff --git a/src/jula/models/models/relation_analyzer.py b/src/jula/models/models/relation_analyzer.py
index 79726cef..45c03767 100644
--- a/src/jula/models/models/relation_analyzer.py
+++ b/src/jula/models/models/relation_analyzer.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import torch
 import torch.nn as nn
 import torch.nn.functional as F
@@ -8,7 +6,8 @@
 
 from jula.models.models.cohesion_analyzer import CohesionAnalyzer, cohesion_cross_entropy_loss
 from jula.models.models.dependency_parser import DependencyParser
-from jula.utils.utils import IGNORE_INDEX
+from jula.models.models.discourse_parser import DiscourseParser
+from jula.utils.constants import IGNORE_INDEX
 
 
 class RelationAnalyzer(nn.Module):
@@ -16,29 +15,33 @@ def __init__(self, hparams: DictConfig, pretrained_model_config: PretrainedConfi
         super().__init__()
         self.hparams = hparams
 
-        self.dependency_parser: DependencyParser = DependencyParser(pretrained_model_config=pretrained_model_config)
-        self.cohesion_analyzer: CohesionAnalyzer = CohesionAnalyzer(
+        self.dependency_parser = DependencyParser(
+            pretrained_model_config=pretrained_model_config,
+            k=hparams.k,
+        )
+        self.cohesion_analyzer = CohesionAnalyzer(
             pretrained_model_config=pretrained_model_config,
             num_rels=int("pas_analysis" in hparams.cohesion_tasks) * len(hparams.cases)
             + int("coreference" in hparams.cohesion_tasks)
             + int("bridging" in hparams.cohesion_tasks),
         )
+        self.discourse_parser = DiscourseParser(pretrained_model_config=pretrained_model_config)
 
     def forward(
         self,
         pooled_outputs: torch.Tensor,
         batch: dict[str, torch.Tensor],
-        inference: Optional[bool] = False,
     ) -> dict[str, torch.Tensor]:
         output: dict[str, torch.Tensor] = dict()
-        # (batch_size, max_seq_len, max_seq_len)
+        # (b, seq, seq)
         dependency_logits, dependency_type_logits = self.dependency_parser(
-            pooled_outputs, dependencies=None if inference else batch["dependencies"]
+            pooled_outputs,
+            dependencies=batch["dependencies"] if batch["training"] else None,
         )
         dependency_logits = torch.where(
             batch["intra_mask"],
             dependency_logits,
-            torch.full_like(dependency_logits, -1e4),
+            torch.full_like(dependency_logits, -1024.0),
         )
         output.update(
             {
@@ -54,8 +57,9 @@ def forward(
             )
             output.update({"dependency_loss": dependency_loss})
         if "dependency_types" in batch:
+            top1 = dependency_type_logits[:, :, 0, :]
             dependency_type_loss = F.cross_entropy(
-                input=dependency_type_logits.view(-1, dependency_type_logits.size(2)),
+                input=top1.view(-1, top1.size(2)),
                 target=batch["dependency_types"].view(-1),
                 ignore_index=IGNORE_INDEX,
             )
@@ -64,7 +68,7 @@ def forward(
         cohesion_logits = self.cohesion_analyzer(pooled_outputs)  # (b, rel, seq, seq)
         output.update(
             {
-                "cohesion_logits": cohesion_logits,
+                "cohesion_logits": cohesion_logits + (~batch["cohesion_mask"]).float() * -1024.0,
             }
         )
         if "cohesion_target" in batch and "cohesion_mask" in batch:
@@ -74,4 +78,25 @@ def forward(
                 batch["cohesion_mask"],
             )
             output.update({"cohesion_loss": cohesion_loss})
+
+        discourse_parsing_logits = self.discourse_parser(pooled_outputs)  # (b, seq, seq, rel)
+        output.update(
+            {
+                "discourse_parsing_logits": discourse_parsing_logits,
+            }
+        )
+        if "discourse_relations" in batch:
+            num_labels = torch.masked_select(
+                batch["discourse_relations"],
+                batch["discourse_relations"] != IGNORE_INDEX,
+            ).numel()
+            if num_labels:
+                discourse_parsing_loss = F.cross_entropy(
+                    input=discourse_parsing_logits.view(-1, discourse_parsing_logits.size(3)),
+                    target=batch["discourse_relations"].view(-1),
+                    ignore_index=IGNORE_INDEX,
+                )
+            else:
+                discourse_parsing_loss = torch.tensor(0.0)
+            output.update({"discourse_parsing_loss": discourse_parsing_loss})
         return output
diff --git a/src/jula/models/models/typo_corrector.py b/src/jula/models/models/typo_corrector.py
index 72e56f8b..65a77e8d 100644
--- a/src/jula/models/models/typo_corrector.py
+++ b/src/jula/models/models/typo_corrector.py
@@ -2,7 +2,7 @@
 import torch.nn as nn
 import torch.nn.functional as F
 from omegaconf import DictConfig
-from transformers import PretrainedConfig, PreTrainedTokenizer
+from transformers import PretrainedConfig, PreTrainedTokenizerBase
 
 
 class TypoCorrector(nn.Module):
@@ -10,7 +10,7 @@ def __init__(
         self,
         hparams: DictConfig,
         pretrained_model_config: PretrainedConfig,
-        tokenizer: PreTrainedTokenizer,
+        tokenizer: PreTrainedTokenizerBase,
     ) -> None:
         super().__init__()
         self.hparams = hparams
diff --git a/src/jula/models/models/word_analyzer.py b/src/jula/models/models/word_analyzer.py
index c79d8c92..43ad4b70 100644
--- a/src/jula/models/models/word_analyzer.py
+++ b/src/jula/models/models/word_analyzer.py
@@ -3,7 +3,7 @@
 import torch.nn.functional as F
 from transformers import PretrainedConfig
 
-from jula.utils.utils import CONJFORM_TYPES, CONJTYPE_TYPES, IGNORE_INDEX, POS_TYPES, SUBPOS_TYPES
+from jula.utils.constants import CONJFORM_TYPES, CONJTYPE_TYPES, IGNORE_INDEX, POS_TYPES, SUBPOS_TYPES
 
 
 class WordAnalyzer(nn.Module):
diff --git a/src/jula/models/models/word_segmenter.py b/src/jula/models/models/word_segmenter.py
index 5fb1b362..1862488a 100644
--- a/src/jula/models/models/word_segmenter.py
+++ b/src/jula/models/models/word_segmenter.py
@@ -4,7 +4,7 @@
 from omegaconf import DictConfig
 from transformers import PretrainedConfig
 
-from jula.utils.utils import IGNORE_INDEX, SEG_TYPES
+from jula.utils.constants import IGNORE_INDEX, SEG_TYPES
 
 
 class WordSegmenter(nn.Module):
diff --git a/src/jula/models/typo_module.py b/src/jula/models/typo_module.py
index 87903dd4..94ebe8b0 100644
--- a/src/jula/models/typo_module.py
+++ b/src/jula/models/typo_module.py
@@ -4,7 +4,7 @@
 import torch
 from omegaconf import DictConfig
 from pytorch_lightning.core.lightning import LightningModule
-from transformers import AutoTokenizer, PretrainedConfig, PreTrainedTokenizer
+from transformers import AutoTokenizer, PretrainedConfig, PreTrainedTokenizerBase
 
 from jula.evaluators.typo_corrector import TypoCorrectorMetric
 from jula.models.models.char_encoder import CharEncoder
@@ -17,7 +17,7 @@ def __init__(self, hparams: DictConfig) -> None:
         self.hparams.update(hparams)
         self.save_hyperparameters()
 
-        self.tokenizer: PreTrainedTokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
             hparams.model.model_name_or_path,
             **hydra.utils.instantiate(hparams.dataset.tokenizer_kwargs, _convert_="partial"),
         )
@@ -38,44 +38,32 @@ def forward(self, **kwargs):
 
     def training_step(self, batch: Any, batch_idx: int) -> dict[str, Any]:
         outputs: dict[str, torch.Tensor] = self(**batch)
-        result: dict[str, Union[torch.Tensor, float]] = self.metrics.compute_metrics(
-            outputs=outputs,
-            batch=batch,
-        )
+        result: dict[str, Union[torch.Tensor, float]] = self.metrics.compute_metrics(outputs, batch)
         for name, value in result.items():
             self.log(f"train/{name}", value)
         return result
 
     def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> dict[str, Any]:
         outputs: dict[str, torch.Tensor] = self(**batch)
-        result: dict[str, Union[torch.Tensor, float]] = self.metrics.compute_metrics(
-            outputs=outputs,
-            batch=batch,
-        )
+        result: dict[str, Union[torch.Tensor, float]] = self.metrics.compute_metrics(outputs, batch)
         for name, value in result.items():
             self.log(f"valid/{name}", value)
         return result
 
     def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> dict[str, Any]:
         outputs: dict[str, torch.Tensor] = self(**batch)
-        result: dict[str, Union[torch.Tensor, float]] = self.metrics.compute_metrics(
-            outputs=outputs,
-            batch=batch,
-        )
+        result: dict[str, Union[torch.Tensor, float]] = self.metrics.compute_metrics(outputs, batch)
         for name, value in result.items():
             self.log(f"test/{name}", value)
         return result
 
     def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Any:
         outputs: dict[str, torch.Tensor] = self(**batch)
-        predict_output = dict(input_ids=batch["input_ids"])
-        for key in batch:
-            if "labels" in key:
-                predict_output[key] = batch[key]
-        for key in outputs:
-            if "logits" in key:
-                predict_output[key] = outputs[key]
-        return predict_output
+        return {
+            "texts": batch["texts"],
+            "kdr_logits": outputs["kdr_logits"],
+            "ins_logits": outputs["ins_logits"],
+        }
 
     def configure_optimizers(self):
         # Split weights in two groups, one with weight decay and the other not.
diff --git a/src/jula/models/word_module.py b/src/jula/models/word_module.py
index 74ad0cfd..25ca9cb7 100644
--- a/src/jula/models/word_module.py
+++ b/src/jula/models/word_module.py
@@ -6,10 +6,11 @@
 import torch
 from omegaconf import DictConfig
 from pytorch_lightning.core.lightning import LightningModule
-from transformers import AutoTokenizer, PreTrainedTokenizerBase
+from transformers import AutoTokenizer, PretrainedConfig, PreTrainedTokenizerBase
 
 from jula.evaluators.cohesion_analysis_metric import CohesionAnalysisMetric
 from jula.evaluators.dependency_parsing_metric import DependencyParsingMetric
+from jula.evaluators.discourse_parsing_metric import DiscourseParsingMetric
 from jula.evaluators.phrase_analysis_metric import PhraseAnalysisMetric
 from jula.evaluators.word_analyzer import WordAnalysisMetric
 from jula.models.models.phrase_analyzer import PhraseAnalyzer
@@ -35,9 +36,9 @@ def __init__(self, hparams: DictConfig) -> None:
 
         self.word_encoder: WordEncoder = WordEncoder(hparams, self.tokenizer)
 
-        self.word_analyzer: WordAnalyzer = WordAnalyzer(
-            pretrained_model_config=self.word_encoder.pretrained_model.config
-        )
+        pretrained_model_config: PretrainedConfig = self.word_encoder.pretrained_model.config
+
+        self.word_analyzer: WordAnalyzer = WordAnalyzer(pretrained_model_config)
         self.valid_word_analysis_metrics: dict[str, WordAnalysisMetric] = {
             corpus: WordAnalysisMetric() for corpus in self.valid_corpora
         }
@@ -45,9 +46,7 @@ def __init__(self, hparams: DictConfig) -> None:
             corpus: WordAnalysisMetric() for corpus in self.test_corpora
         }
 
-        self.phrase_analyzer: PhraseAnalyzer = PhraseAnalyzer(
-            pretrained_model_config=self.word_encoder.pretrained_model.config,
-        )
+        self.phrase_analyzer: PhraseAnalyzer = PhraseAnalyzer(pretrained_model_config)
         self.valid_phrase_analysis_metrics: dict[str, PhraseAnalysisMetric] = {
             corpus: PhraseAnalysisMetric() for corpus in self.valid_corpora
         }
@@ -55,10 +54,7 @@ def __init__(self, hparams: DictConfig) -> None:
             corpus: PhraseAnalysisMetric() for corpus in self.test_corpora
         }
 
-        self.relation_analyzer: RelationAnalyzer = RelationAnalyzer(
-            hparams=hparams,
-            pretrained_model_config=self.word_encoder.pretrained_model.config,
-        )
+        self.relation_analyzer: RelationAnalyzer = RelationAnalyzer(hparams, pretrained_model_config)
         self.valid_dependency_parsing_metrics: dict[str, DependencyParsingMetric] = {
             corpus: DependencyParsingMetric() for corpus in self.valid_corpora
         }
@@ -71,13 +67,19 @@ def __init__(self, hparams: DictConfig) -> None:
         self.test_cohesion_analysis_metrics: dict[str, CohesionAnalysisMetric] = {
             corpus: CohesionAnalysisMetric() for corpus in self.test_corpora
         }
+        self.valid_discourse_parsing_metrics: dict[str, DiscourseParsingMetric] = {
+            corpus: DiscourseParsingMetric() for corpus in self.valid_corpora
+        }
+        self.test_discourse_parsing_metrics: dict[str, DiscourseParsingMetric] = {
+            corpus: DiscourseParsingMetric() for corpus in self.test_corpora
+        }
 
-    def forward(self, inference=False, **batch) -> dict[str, dict[str, torch.Tensor]]:
+    def forward(self, **batch) -> dict[str, dict[str, torch.Tensor]]:
         # (batch_size, seq_len, hidden_size)
         pooled_outputs = self.word_encoder(batch, PoolingStrategy.FIRST)
         word_analyzer_outputs = self.word_analyzer(pooled_outputs, batch)
         phrase_analyzer_outputs = self.phrase_analyzer(pooled_outputs, batch)
-        relation_analyzer_output = self.relation_analyzer(pooled_outputs, batch, inference=inference)
+        relation_analyzer_output = self.relation_analyzer(pooled_outputs, batch)
         return {
             "word_analyzer_outputs": word_analyzer_outputs,
             "phrase_analyzer_outputs": phrase_analyzer_outputs,
@@ -85,44 +87,22 @@ def forward(self, inference=False, **batch) -> dict[str, dict[str, torch.Tensor]
         }
 
     def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
-        outputs: dict[str, torch.Tensor] = self(inference=False, **batch)
+        batch["training"] = True
+        outputs: dict[str, torch.Tensor] = self(**batch)
         word_analysis_loss = outputs["word_analyzer_outputs"]["loss"]
-        self.log(
-            "train/word_analysis_loss",
-            word_analysis_loss,
-            on_step=True,
-            on_epoch=False,
-        )
+        self.log("train/word_analysis_loss", word_analysis_loss)
         word_feature_loss = outputs["phrase_analyzer_outputs"]["word_feature_loss"]
-        self.log(
-            "train/word_feature_loss",
-            word_feature_loss,
-            on_step=True,
-            on_epoch=False,
-        )
+        self.log("train/word_feature_loss", word_feature_loss)
         base_phrase_feature_loss = outputs["phrase_analyzer_outputs"]["base_phrase_feature_loss"]
-        self.log(
-            "train/base_phrase_feature_loss",
-            base_phrase_feature_loss,
-            on_step=True,
-            on_epoch=False,
-        )
+        self.log("train/base_phrase_feature_loss", base_phrase_feature_loss)
         dependency_loss = outputs["relation_analyzer_outputs"]["dependency_loss"]
-        self.log("train/dependency_loss", dependency_loss, on_step=True, on_epoch=True)
+        self.log("train/dependency_loss", dependency_loss)
         dependency_type_loss = outputs["relation_analyzer_outputs"]["dependency_type_loss"]
-        self.log(
-            "train/dependency_type_loss",
-            dependency_type_loss,
-            on_step=True,
-            on_epoch=False,
-        )
+        self.log("train/dependency_type_loss", dependency_type_loss)
         cohesion_loss = outputs["relation_analyzer_outputs"]["cohesion_loss"]
-        self.log(
-            "train/cohesion_loss",
-            cohesion_loss,
-            on_step=True,
-            on_epoch=False,
-        )
+        self.log("train/cohesion_loss", cohesion_loss)
+        discourse_parsing_loss = outputs["relation_analyzer_outputs"]["discourse_parsing_loss"]
+        self.log("train/discourse_parsing_loss", discourse_parsing_loss)
         return (
             word_analysis_loss
             + word_feature_loss
@@ -130,10 +110,12 @@ def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
             + dependency_loss
             + dependency_type_loss
             + cohesion_loss
+            + discourse_parsing_loss
         )
 
     def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
-        outputs: dict[str, torch.Tensor] = self(inference=True, **batch)
+        batch["training"] = False
+        outputs: dict[str, torch.Tensor] = self(**batch)
         corpus = self.valid_corpora[dataloader_idx or 0]
         word_analysis_metric_args = {
             "pos_preds": torch.argmax(outputs["word_analyzer_outputs"]["pos_logits"], dim=-1),
@@ -146,10 +128,7 @@ def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[i
             "conjform_labels": batch["mrph_types"][:, :, 3],
         }
         self.valid_word_analysis_metrics[corpus].update(**word_analysis_metric_args)
-        self.log(
-            "valid/word_analysis_loss",
-            outputs["word_analyzer_outputs"]["loss"],
-        )
+        self.log("valid/word_analysis_loss", outputs["word_analyzer_outputs"]["loss"])
 
         phrase_analysis_metric_args = {
             "example_ids": batch["example_ids"],
@@ -161,25 +140,23 @@ def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[i
             "base_phrase_features": batch["base_phrase_features"],
         }
         self.valid_phrase_analysis_metrics[corpus].update(**phrase_analysis_metric_args)
-        self.log(
-            "valid/word_feature_loss",
-            outputs["phrase_analyzer_outputs"]["word_feature_loss"],
-        )
-        self.log(
-            "valid/base_phrase_feature_loss",
-            outputs["phrase_analyzer_outputs"]["base_phrase_feature_loss"],
-        )
+        self.log("valid/word_feature_loss", outputs["phrase_analyzer_outputs"]["word_feature_loss"])
+        self.log("valid/base_phrase_feature_loss", outputs["phrase_analyzer_outputs"]["base_phrase_feature_loss"])
 
         dependency_parsing_metric_args = {
             "example_ids": batch["example_ids"],
-            "preds": torch.argmax(outputs["relation_analyzer_outputs"]["dependency_logits"], dim=2),
-            "type_preds": torch.argmax(outputs["relation_analyzer_outputs"]["dependency_type_logits"], dim=2),
+            "dependency_predictions": torch.topk(
+                outputs["relation_analyzer_outputs"]["dependency_logits"],
+                self.hparams.k,
+                dim=2,
+            ).indices,
+            "dependency_type_predictions": torch.argmax(
+                outputs["relation_analyzer_outputs"]["dependency_type_logits"],
+                dim=3,
+            ),
         }
         self.valid_dependency_parsing_metrics[corpus].update(**dependency_parsing_metric_args)
-        self.log(
-            "valid/dependency_loss",
-            outputs["relation_analyzer_outputs"]["dependency_loss"],
-        )
+        self.log("valid/dependency_loss", outputs["relation_analyzer_outputs"]["dependency_loss"])
 
         cohesion_analysis_metric_args = {
             "example_ids": batch["example_ids"],
@@ -187,10 +164,17 @@ def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[i
             "dataset": self.trainer.val_dataloaders[dataloader_idx or 0].dataset,
         }
         self.valid_cohesion_analysis_metrics[corpus].update(**cohesion_analysis_metric_args)
-        self.log(
-            "valid/cohesion_loss",
-            outputs["relation_analyzer_outputs"]["cohesion_loss"],
-        )
+        self.log("valid/cohesion_loss", outputs["relation_analyzer_outputs"]["cohesion_loss"])
+
+        discourse_parsing_metric_args = {
+            "discourse_parsing_predictions": torch.argmax(
+                outputs["relation_analyzer_outputs"]["discourse_parsing_logits"],
+                dim=-1,
+            ),
+            "discourse_parsing_labels": batch["discourse_relations"],
+        }
+        self.valid_discourse_parsing_metrics[corpus].update(**discourse_parsing_metric_args)
+        self.log("valid/discourse_parsing_loss", outputs["relation_analyzer_outputs"]["discourse_parsing_loss"])
 
     def validation_epoch_end(self, validation_step_outputs) -> None:
         f1_scores: dict[str, float] = defaultdict(float)
@@ -201,10 +185,7 @@ def validation_epoch_end(self, validation_step_outputs) -> None:
                     f1_scores["word_analysis_f1"] += value / len(self.valid_word_analysis_metrics)
                 self.log(f"valid_{corpus}/{name}", value)
                 metric.reset()
-        self.log(
-            "valid/word_analysis_f1",
-            f1_scores["word_analysis_f1"],
-        )
+        self.log("valid/word_analysis_f1", f1_scores["word_analysis_f1"])
 
         keys = {
             "macro_word_feature_f1",
@@ -226,7 +207,8 @@ def validation_epoch_end(self, validation_step_outputs) -> None:
         for idx, corpus in enumerate(self.valid_corpora):
             dataset = self.trainer.val_dataloaders[idx].dataset
             metric = self.valid_dependency_parsing_metrics[corpus]
-            for name, value in metric.compute(dataset).items():
+            documents = [dataset.doc_id2document[example.doc_id] for example in dataset.examples]
+            for name, value in metric.compute(documents).items():
                 self.log(f"valid_{corpus}/{name}", value)
             metric.reset()
 
@@ -238,8 +220,15 @@ def validation_epoch_end(self, validation_step_outputs) -> None:
                     self.log(f"valid_{corpus}/{met}_{rel}", sub_val.f1)
             metric.reset()
 
+        for idx, corpus in enumerate(self.valid_corpora):
+            metric = self.valid_discourse_parsing_metrics[corpus]
+            for name, value in metric.compute().items():
+                self.log(f"valid_{corpus}/{name}", value)
+            metric.reset()
+
     def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
-        outputs: dict[str, torch.Tensor] = self(inference=True, **batch)
+        batch["training"] = False
+        outputs: dict[str, torch.Tensor] = self(**batch)
         corpus = self.test_corpora[dataloader_idx or 0]
         word_analysis_metric_args = {
             "pos_preds": torch.argmax(outputs["word_analyzer_outputs"]["pos_logits"], dim=-1),
@@ -252,10 +241,7 @@ def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] =
             "conjform_labels": batch["mrph_types"][:, :, 3],
         }
         self.test_word_analysis_metrics[corpus].update(**word_analysis_metric_args)
-        self.log(
-            "test/word_analysis_loss",
-            outputs["word_analyzer_outputs"]["loss"],
-        )
+        self.log("test/word_analysis_loss", outputs["word_analyzer_outputs"]["loss"])
 
         phrase_analysis_metric_args = {
             "example_ids": batch["example_ids"],
@@ -267,25 +253,23 @@ def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] =
             "base_phrase_features": batch["base_phrase_features"],
         }
         self.test_phrase_analysis_metrics[corpus].update(**phrase_analysis_metric_args)
-        self.log(
-            "test/word_feature_loss",
-            outputs["phrase_analyzer_outputs"]["word_feature_loss"],
-        )
-        self.log(
-            "test/base_phrase_feature_loss",
-            outputs["phrase_analyzer_outputs"]["base_phrase_feature_loss"],
-        )
+        self.log("test/word_feature_loss", outputs["phrase_analyzer_outputs"]["word_feature_loss"])
+        self.log("test/base_phrase_feature_loss", outputs["phrase_analyzer_outputs"]["base_phrase_feature_loss"])
 
         dependency_parsing_metric_args = {
             "example_ids": batch["example_ids"],
-            "preds": torch.argmax(outputs["relation_analyzer_outputs"]["dependency_logits"], dim=2),
-            "type_preds": torch.argmax(outputs["relation_analyzer_outputs"]["dependency_type_logits"], dim=2),
+            "dependency_predictions": torch.topk(
+                outputs["relation_analyzer_outputs"]["dependency_logits"],
+                self.hparams.k,
+                dim=2,
+            ).indices,
+            "dependency_type_predictions": torch.argmax(
+                outputs["relation_analyzer_outputs"]["dependency_type_logits"],
+                dim=3,
+            ),
         }
         self.test_dependency_parsing_metrics[corpus].update(**dependency_parsing_metric_args)
-        self.log(
-            "test/dependency_loss",
-            outputs["relation_analyzer_outputs"]["dependency_loss"],
-        )
+        self.log("test/dependency_loss", outputs["relation_analyzer_outputs"]["dependency_loss"])
 
         cohesion_analysis_metric_args = {
             "example_ids": batch["example_ids"],
@@ -293,10 +277,17 @@ def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] =
             "dataset": self.trainer.test_dataloaders[dataloader_idx or 0].dataset,
         }
         self.test_cohesion_analysis_metrics[corpus].update(**cohesion_analysis_metric_args)
-        self.log(
-            "test/cohesion_loss",
-            outputs["relation_analyzer_outputs"]["cohesion_loss"],
-        )
+        self.log("test/cohesion_loss", outputs["relation_analyzer_outputs"]["cohesion_loss"])
+
+        discourse_parsing_metric_args = {
+            "discourse_parsing_predictions": torch.argmax(
+                outputs["relation_analyzer_outputs"]["discourse_parsing_logits"],
+                dim=-1,
+            ),
+            "discourse_parsing_labels": batch["discourse_relations"],
+        }
+        self.test_discourse_parsing_metrics[corpus].update(**discourse_parsing_metric_args)
+        self.log("test/discourse_parsing_loss", outputs["relation_analyzer_outputs"]["discourse_parsing_loss"])
 
     def test_epoch_end(self, test_step_outputs) -> None:
         f1_scores: dict[str, float] = defaultdict(float)
@@ -307,10 +298,7 @@ def test_epoch_end(self, test_step_outputs) -> None:
                     f1_scores[name] += value / len(self.test_word_analysis_metrics)
                 self.log(f"test_{corpus}/{name}", value)
                 metric.reset()
-        self.log(
-            "test/word_analysis_f1",
-            f1_scores["word_analysis_f1"],
-        )
+        self.log("test/word_analysis_f1", f1_scores["word_analysis_f1"])
 
         keys = {
             "macro_word_feature_f1",
@@ -332,7 +320,8 @@ def test_epoch_end(self, test_step_outputs) -> None:
         for idx, corpus in enumerate(self.test_corpora):
             dataset = self.trainer.test_dataloaders[idx].dataset
             metric = self.test_dependency_parsing_metrics[corpus]
-            for name, value in metric.compute(dataset).items():
+            documents = [dataset.doc_id2document[example.doc_id] for example in dataset.examples]
+            for name, value in metric.compute(documents).items():
                 self.log(f"test_{corpus}/{name}", value)
             metric.reset()
 
@@ -344,10 +333,17 @@ def test_epoch_end(self, test_step_outputs) -> None:
                     self.log(f"test_{corpus}/{met}_{rel}", sub_val.f1)
             metric.reset()
 
+        for idx, corpus in enumerate(self.valid_corpora):
+            metric = self.test_discourse_parsing_metrics[corpus]
+            for name, value in metric.compute().items():
+                self.log(f"test_{corpus}/{name}", value)
+            metric.reset()
+
     def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Any:
-        outputs: dict[str, torch.Tensor] = self(inference=True, **batch)
+        batch["training"] = False
+        outputs: dict[str, torch.Tensor] = self(**batch)
         return {
-            "example_ids": batch["example_ids"],
+            "texts": batch["texts"],
             "word_analysis_pos_logits": outputs["word_analyzer_outputs"]["pos_logits"],
             "word_analysis_subpos_logits": outputs["word_analyzer_outputs"]["subpos_logits"],
             "word_analysis_conjtype_logits": outputs["word_analyzer_outputs"]["conjtype_logits"],
@@ -356,6 +352,8 @@ def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int]
             "base_phrase_feature_logits": outputs["phrase_analyzer_outputs"]["base_phrase_feature_logits"],
             "dependency_logits": outputs["relation_analyzer_outputs"]["dependency_logits"],
             "dependency_type_logits": outputs["relation_analyzer_outputs"]["dependency_type_logits"],
+            "cohesion_logits": outputs["relation_analyzer_outputs"]["cohesion_logits"],
+            "discourse_parsing_logits": outputs["relation_analyzer_outputs"]["discourse_parsing_logits"],
         }
 
     def configure_optimizers(self):
@@ -391,9 +389,5 @@ def configure_optimizers(self):
         )
         return {
             "optimizer": optimizer,
-            "lr_scheduler": {
-                "scheduler": lr_scheduler,
-                "interval": "step",
-                "frequency": 1,
-            },
+            "lr_scheduler": {"scheduler": lr_scheduler, "interval": "step", "frequency": 1},
         }
diff --git a/src/jula/utils/utils.py b/src/jula/utils/constants.py
similarity index 81%
rename from src/jula/utils/utils.py
rename to src/jula/utils/constants.py
index b02990a8..4458d042 100644
--- a/src/jula/utils/utils.py
+++ b/src/jula/utils/constants.py
@@ -1,5 +1,7 @@
 import itertools
 
+from rhoknp.units.utils import DepType
+
 TYPO_OPN2TOKEN = {
     "K": "<k>",
     "D": "<d>",
@@ -236,13 +238,41 @@
 IGNORE_INDEX = -100
 
 
-DEPENDENCY_TYPES = (
-    "D",
-    "P",
-    "A",
-    "I",
-)
-INDEX2DEPENDENCY_TYPE = {index: dependency_type for index, dependency_type in enumerate(DEPENDENCY_TYPES)}
+DEPENDENCY_TYPE2INDEX: dict[DepType, int] = {
+    DepType.DEPENDENCY: 0,
+    DepType.PARALLEL: 1,
+    DepType.APPOSITION: 2,
+    DepType.IMPERFECT_PARALLEL: 3,
+}
+INDEX2DEPENDENCY_TYPE: dict[int, DepType] = {
+    index: dependency_type for dependency_type, index in DEPENDENCY_TYPE2INDEX.items()
+}
+
+
+DISCOURSE_RELATION_MAP = {
+    # Labels annotated by experts.
+    "その他根拠(逆方向)": "根拠",
+    "その他根拠(順方向)": "根拠",
+    "原因・理由(逆方向)": "原因・理由",
+    "原因・理由(順方向)": "原因・理由",
+    "対比(方向なし)": "対比",
+    "条件(逆方向)": "条件",
+    "条件(順方向)": "条件",
+    "目的(逆方向)": "目的",
+    "目的(順方向)": "目的",
+    "談話関係なし": "談話関係なし",
+    "逆接・譲歩(逆方向)": "逆接",
+    "逆接・譲歩(順方向)": "逆接",
+    # Labels annotated by crowd-workers.
+    "NIL": "談話関係なし",
+    "上記いずれの関係もない": "談話関係なし",
+    "原因・理由": "原因・理由",
+    "対比": "対比",
+    "条件": "条件",
+    "根拠": "根拠",
+    "目的": "目的",
+    "逆接": "逆接",
+}
 
 
 DISCOURSE_RELATIONS = (
@@ -254,3 +284,4 @@
     "対比",
     "逆接",
 )
+INDEX2DISCOURSE_RELATION = {index: discourse_relation for index, discourse_relation in enumerate(DISCOURSE_RELATIONS)}
diff --git a/src/jula/writer/knp.py b/src/jula/writer/cohesion.py
similarity index 89%
rename from src/jula/writer/knp.py
rename to src/jula/writer/cohesion.py
index cbfb1e92..a8c7bbb9 100644
--- a/src/jula/writer/knp.py
+++ b/src/jula/writer/cohesion.py
@@ -6,12 +6,12 @@
 from rhoknp import BasePhrase, Document
 from rhoknp.rel import ExophoraReferent
 from rhoknp.rel.pas import Argument, BaseArgument, Pas, SpecialArgument
-from rhoknp.units.utils import Rel
+from rhoknp.units.utils import Rel, Rels
 
 from jula.datamodule.datasets.word_dataset import WordDataset
-from jula.datamodule.examples import Task
+from jula.datamodule.examples import CohesionExample, Task
 
-logger = logging.getLogger(__file__)
+logger = logging.getLogger(__name__)
 
 
 class CohesionKNPWriter:
@@ -32,6 +32,7 @@ def __init__(self, dataset: WordDataset) -> None:
         self.exophora_referents: list[ExophoraReferent] = dataset.exophora_referents
         self.specials: list[str] = dataset.special_tokens
         self.documents: list[Document] = [Document.from_knp(doc.to_knp()) for doc in dataset.documents]
+        self.examples: list[CohesionExample] = [e.cohesion_example for e in dataset.examples]
         self.kc: bool = False
 
     def write(
@@ -58,7 +59,7 @@ def write(
         elif not (destination is None or isinstance(destination, io.TextIOBase)):
             logger.warning("invalid output destination")
 
-        did2prediction: dict[str, list] = {self.documents[eid].doc_id: pred for eid, pred in predictions.items()}
+        did2prediction: dict[str, list] = {self.examples[eid].doc_id: pred for eid, pred in predictions.items()}
         documents = []
         for document in self.documents:
             did = document.doc_id
@@ -90,16 +91,16 @@ def write(
     def _to_rels(
         self,
         prediction: list[int],  # (rel)
-        bp_list: list[BasePhrase],
-    ) -> list[Rel]:
-        rels: list[Rel] = []
+        base_phrases: list[BasePhrase],
+    ) -> Rels:
+        rels = Rels()
         assert len(self.relations) == len(prediction)
         for relation, pred in zip(self.relations, prediction):
             if pred < 0:
                 continue  # non-target phrase
-            if 0 <= pred < len(bp_list):
+            if 0 <= pred < len(base_phrases):
                 # normal
-                prediction_bp: BasePhrase = bp_list[pred]
+                prediction_bp: BasePhrase = base_phrases[pred]
                 rels.append(
                     Rel(
                         type=relation,
@@ -109,9 +110,9 @@ def _to_rels(
                         mode=None,
                     )
                 )
-            elif 0 <= pred - len(bp_list) < len(self.specials):
+            elif 0 <= pred - len(base_phrases) < len(self.specials):
                 # special
-                special_arg = self.specials[pred - len(bp_list)]
+                special_arg = self.specials[pred - len(base_phrases)]
                 if special_arg in [str(e) for e in self.exophora_referents]:  # exclude [NULL] and [NA]
                     rels.append(
                         Rel(
@@ -160,10 +161,10 @@ def _pas_string(
         for case in self.cases + ["ノ"] * (Task.BRIDGING in self.tasks):
             items = ["-"] * 6
             items[0] = case
-            args = pas.arguments[case]
+            args = pas.get_arguments(case, relax=False)
             if args:
                 arg: BaseArgument = args[0]
-                items[1] = str(arg.type)  # フラグ (C/N/O/D/E/U)
+                items[1] = arg.type.value  # フラグ (C/N/O/D/E/U)
                 items[2] = str(arg)  # 見出し
                 if isinstance(arg, Argument):
                     items[3] = str(sid2index[pas.sid] - sid2index[arg.base_phrase.sentence.sid])  # N文前


Test patch:
diff --git a/tests/callbacks/test_typo_corrector_writer.py b/tests/callbacks/test_typo_corrector_writer.py
new file mode 100644
index 00000000..9a5606ea
--- /dev/null
+++ b/tests/callbacks/test_typo_corrector_writer.py
@@ -0,0 +1,68 @@
+import tempfile
+
+import torch
+
+from jula.callbacks.typo_corrector_writer import TypoCorrectorWriter
+from jula.utils.constants import TYPO_OPN2TOKEN
+
+
+def test_init():
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        _ = TypoCorrectorWriter(
+            tmp_dir,
+            extended_vocab_path="tests/datamodule/datasets/typo_files/extended_vocab.txt",
+            tokenizer_kwargs={
+                "do_word_tokenize": False,
+                "additional_special_tokens": ["<k>", "<d>", "<_>", "<dummy>"],
+            },
+        )
+
+
+def test_write_on_epoch_end():
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        writer = TypoCorrectorWriter(
+            tmp_dir,
+            extended_vocab_path="tests/datamodule/datasets/typo_files/extended_vocab.txt",
+            tokenizer_kwargs={
+                "do_word_tokenize": False,
+                "additional_special_tokens": ["<k>", "<d>", "<_>", "<dummy>"],
+            },
+        )
+
+        # tokenizer = writer.tokenizer
+        texts = ["今日がも晴れ"]
+
+        kdr_logits = torch.zeros(1, 9, len(writer.opn2id), dtype=torch.float)
+        kdr_logits[0][0][writer.opn2id[TYPO_OPN2TOKEN["K"]]] = 1.0  # keep: [CLS]
+        kdr_logits[0][1][writer.opn2id[TYPO_OPN2TOKEN["K"]]] = 1.0  # keep: 今
+        kdr_logits[0][2][writer.opn2id[TYPO_OPN2TOKEN["K"]]] = 1.0  # keep: 日
+        kdr_logits[0][3][writer.opn2id["は"]] = 1.0  # replace: が -> は
+        kdr_logits[0][4][writer.opn2id[TYPO_OPN2TOKEN["D"]]] = 1.0  # delete: も ->
+        kdr_logits[0][5][writer.opn2id[TYPO_OPN2TOKEN["K"]]] = 1.0  # keep: 晴
+        kdr_logits[0][6][writer.opn2id[TYPO_OPN2TOKEN["K"]]] = 1.0  # keep: れ
+        kdr_logits[0][7][writer.opn2id[TYPO_OPN2TOKEN["K"]]] = 1.0  # insert: <dummy> -> だ
+        kdr_logits[0][8][writer.opn2id[TYPO_OPN2TOKEN["K"]]] = 1.0  # keep: [SEP]
+
+        ins_logits = torch.zeros(1, 9, len(writer.opn2id), dtype=torch.float)
+        ins_logits[0][0][writer.opn2id[TYPO_OPN2TOKEN["_"]]] = 1.0  # keep: [CLS]
+        ins_logits[0][1][writer.opn2id[TYPO_OPN2TOKEN["_"]]] = 1.0  # keep: 今
+        ins_logits[0][2][writer.opn2id[TYPO_OPN2TOKEN["_"]]] = 1.0  # keep: 日
+        ins_logits[0][3][writer.opn2id[TYPO_OPN2TOKEN["_"]]] = 1.0  # replace: が -> は
+        ins_logits[0][4][writer.opn2id[TYPO_OPN2TOKEN["_"]]] = 1.0  # delete: も ->
+        ins_logits[0][5][writer.opn2id[TYPO_OPN2TOKEN["_"]]] = 1.0  # keep: 晴
+        ins_logits[0][6][writer.opn2id[TYPO_OPN2TOKEN["_"]]] = 1.0  # keep: れ
+        ins_logits[0][7][writer.opn2id["だ"]] = 1.0  # insert: <dummy> -> だ
+        ins_logits[0][8][writer.opn2id[TYPO_OPN2TOKEN["_"]]] = 1.0  # keep: [SEP]
+
+        predictions = [
+            [
+                {
+                    "texts": texts,
+                    "kdr_logits": kdr_logits,
+                    "ins_logits": ins_logits,
+                }
+            ]
+        ]
+        writer.write_on_epoch_end(..., ..., predictions)
+        with open(writer.output_path) as f:
+            assert f.read().strip() == "今日は晴れだ"
diff --git a/tests/callbacks/test_word_module_writer.py b/tests/callbacks/test_word_module_writer.py
new file mode 100644
index 00000000..ec012797
--- /dev/null
+++ b/tests/callbacks/test_word_module_writer.py
@@ -0,0 +1,131 @@
+import tempfile
+import textwrap
+from pathlib import Path
+
+import torch
+from rhoknp.units.utils import DepType
+
+import jula
+from jula.callbacks.word_module_writer import WordModuleWriter
+from jula.utils.constants import (
+    BASE_PHRASE_FEATURES,
+    CONJFORM_TYPES,
+    CONJTYPE_TYPES,
+    DEPENDENCY_TYPE2INDEX,
+    POS_TYPES,
+    SUBPOS_TYPES,
+    WORD_FEATURES,
+)
+
+
+def test_init():
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        _ = WordModuleWriter(
+            tmp_dir,
+            tokenizer_kwargs={
+                "additional_special_tokens": ["著者", "読者", "不特定:人", "不特定:物", "[NULL]", "[NA]", "[ROOT]"],
+            },
+        )
+
+
+def test_write_on_epoch_end():
+    texts = ["今日 は 晴れ だ"]
+
+    word_analysis_pos_logits = torch.zeros(1, 11, len(POS_TYPES), dtype=torch.float)
+    word_analysis_pos_logits[0][0][POS_TYPES.index("名詞")] = 1.0
+    word_analysis_pos_logits[0][1][POS_TYPES.index("助詞")] = 1.0
+    word_analysis_pos_logits[0][2][POS_TYPES.index("名詞")] = 1.0
+    word_analysis_pos_logits[0][3][POS_TYPES.index("判定詞")] = 1.0
+
+    word_analysis_subpos_logits = torch.zeros(1, 11, len(SUBPOS_TYPES), dtype=torch.float)
+    word_analysis_subpos_logits[0][0][SUBPOS_TYPES.index("時相名詞")] = 1.0
+    word_analysis_subpos_logits[0][1][SUBPOS_TYPES.index("副助詞")] = 1.0
+    word_analysis_subpos_logits[0][2][SUBPOS_TYPES.index("普通名詞")] = 1.0
+    word_analysis_subpos_logits[0][3][SUBPOS_TYPES.index("*")] = 1.0
+
+    word_analysis_conjtype_logits = torch.zeros(1, 11, len(CONJTYPE_TYPES), dtype=torch.float)
+    word_analysis_conjtype_logits[0][0][CONJTYPE_TYPES.index("*")] = 1.0
+    word_analysis_conjtype_logits[0][1][CONJTYPE_TYPES.index("*")] = 1.0
+    word_analysis_conjtype_logits[0][2][CONJTYPE_TYPES.index("*")] = 1.0
+    word_analysis_conjtype_logits[0][3][CONJTYPE_TYPES.index("判定詞")] = 1.0
+
+    word_analysis_conjform_logits = torch.zeros(1, 11, len(CONJFORM_TYPES), dtype=torch.float)
+    word_analysis_conjform_logits[0][0][CONJFORM_TYPES.index("*")] = 1.0
+    word_analysis_conjform_logits[0][1][CONJFORM_TYPES.index("*")] = 1.0
+    word_analysis_conjform_logits[0][2][CONJFORM_TYPES.index("*")] = 1.0
+    word_analysis_conjform_logits[0][3][CONJFORM_TYPES.index("基本形")] = 1.0
+
+    word_feature_logits = torch.zeros(1, 11, len(WORD_FEATURES), dtype=torch.float)
+    word_feature_logits[0][0][WORD_FEATURES.index("基本句-主辞")] = 1.0
+    word_feature_logits[0][1][WORD_FEATURES.index("基本句-区切")] = 1.0
+    word_feature_logits[0][1][WORD_FEATURES.index("文節-区切")] = 1.0
+    word_feature_logits[0][2][WORD_FEATURES.index("基本句-主辞")] = 1.0
+    word_feature_logits[0][3][WORD_FEATURES.index("基本句-区切")] = 1.0
+    word_feature_logits[0][3][WORD_FEATURES.index("文節-区切")] = 1.0
+
+    base_phrase_feature_logits = torch.zeros(1, 11, len(BASE_PHRASE_FEATURES), dtype=torch.float)
+    base_phrase_feature_logits[0][0][BASE_PHRASE_FEATURES.index("体言")] = 1.0
+    base_phrase_feature_logits[0][2][BASE_PHRASE_FEATURES.index("用言:判")] = 1.0
+    base_phrase_feature_logits[0][2][BASE_PHRASE_FEATURES.index("時制:非過去")] = 1.0
+    base_phrase_feature_logits[0][2][BASE_PHRASE_FEATURES.index("節-主辞")] = 1.0
+    base_phrase_feature_logits[0][2][BASE_PHRASE_FEATURES.index("節-区切")] = 1.0
+
+    dependency_logits = torch.zeros(1, 4, 11, dtype=torch.float)  # (b, word, word)
+    dependency_logits[0][0][2] = 1.0
+    dependency_logits[0][1][1] = 1.0
+    dependency_logits[0][2][10] = 1.0  # [ROOT]
+    dependency_logits[0][3][2] = 1.0
+
+    dependency_type_logits = torch.zeros(1, 11, 1, len(DEPENDENCY_TYPE2INDEX), dtype=torch.float)
+    dependency_type_logits[0][0][0][DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]] = 1.0
+    dependency_type_logits[0][1][0][DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]] = 1.0
+    dependency_type_logits[0][2][0][DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]] = 1.0
+    dependency_type_logits[0][3][0][DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]] = 1.0
+
+    cohesion_logits = torch.zeros(1, 6, 4, 11, dtype=torch.float)  # (b, rel, word, word)
+    cohesion_logits[0, :, :, 8] = 1.0  # [NULL]
+    cohesion_logits[0, :, :, 9] = 1.0  # [NA]
+    cohesion_logits[0, 0, 2, 0] = 2.0  # 今日 ガ 晴れ
+
+    # NOTE: This is not a correct prediction, but it is used to test the module.
+    discourse_parsing_logits = torch.zeros(1, 4, 11, 7, dtype=torch.float)  # (b, word, word, rel)
+    discourse_parsing_logits[0][2][2][1] = 1.0  # 晴れ -> 晴れ: 原因・理由
+
+    predictions = [
+        [
+            {
+                "texts": texts,
+                "word_analysis_pos_logits": word_analysis_pos_logits,
+                "word_analysis_subpos_logits": word_analysis_subpos_logits,
+                "word_analysis_conjtype_logits": word_analysis_conjtype_logits,
+                "word_analysis_conjform_logits": word_analysis_conjform_logits,
+                "word_feature_logits": word_feature_logits,
+                "base_phrase_feature_logits": base_phrase_feature_logits,
+                "dependency_logits": dependency_logits,
+                "dependency_type_logits": dependency_type_logits,
+                "cohesion_logits": cohesion_logits,
+                "discourse_parsing_logits": discourse_parsing_logits,
+            }
+        ]
+    ]
+
+    pred_filename = "test"
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        # max_seq_length = 4 (今日, は, 晴れ, だ) + 7 (著者, 読者, 不特定:人, 不特定:物, [NULL], [NA], [ROOT])
+        writer = WordModuleWriter(tmp_dir, pred_filename=pred_filename, max_seq_length=11)
+        writer.write_on_epoch_end(..., ..., predictions)
+        expected_knp = textwrap.dedent(
+            f"""\
+            # S-ID:1 jula:{jula.__version__}
+            *
+            + 1D <体言>
+            今日 今日 今日 名詞 0 時相名詞 0 * 0 * 0 <基本句-主辞>
+            は は は 助詞 0 副助詞 0 * 0 * 0
+            *
+            + -1D <rel type="ガ" target="今日" sid="1" id="0"/><用言:判><時制:非過去><節-主辞><節-区切><談話関係:1/1/原因・理由>
+            晴れ 晴れ 晴れ 名詞 0 普通名詞 0 * 0 * 0 <基本句-主辞>
+            だ だ だ 判定詞 0 * 0 判定詞 0 基本形 0
+            EOS
+            """
+        )
+        assert Path(tmp_dir).joinpath(f"{pred_filename}.knp").read_text() == expected_knp
diff --git a/tests/callbacks/test_word_segmenter_writer.py b/tests/callbacks/test_word_segmenter_writer.py
new file mode 100644
index 00000000..408711d5
--- /dev/null
+++ b/tests/callbacks/test_word_segmenter_writer.py
@@ -0,0 +1,43 @@
+import tempfile
+
+import torch
+
+from jula.callbacks.word_segmenter_writer import WordSegmenterWriter
+
+
+def test_init():
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        _ = WordSegmenterWriter(tmp_dir)
+
+
+def test_write_on_epoch_end():
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        writer = WordSegmenterWriter(tmp_dir)
+
+        tokenizer = writer.tokenizer
+        input_ids = tokenizer("今日は晴れ", return_tensors="pt")["input_ids"]
+        word_segmenter_logits = torch.tensor(
+            [
+                [
+                    [0.0, 0.0],  # CLS
+                    [1.0, 0.0],  # 今
+                    [0.0, 1.0],  # 日
+                    [1.0, 0.0],  # は
+                    [1.0, 0.0],  # 晴
+                    [0.0, 1.0],  # れ
+                    [0.0, 0.0],  # SEP
+                ]
+            ],
+            dtype=torch.float,
+        )
+        predictions = [
+            [
+                {
+                    "input_ids": input_ids,
+                    "word_segmenter_logits": word_segmenter_logits,
+                }
+            ]
+        ]
+        writer.write_on_epoch_end(..., ..., predictions)
+        with open(writer.output_path) as f:
+            assert f.read().strip() == "今日 は 晴れ"
diff --git a/tests/conftest.py b/tests/conftest.py
index a7a05fa4..68182ecc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,10 @@
 from pathlib import Path
 
 import pytest
+from rhoknp import Document
+from rhoknp.rel import ExophoraReferent
+
+from jula.evaluators.cohesion_scorer import Scorer
 
 here = Path(__file__).parent
 
@@ -8,3 +12,21 @@
 @pytest.fixture()
 def fixture_data_dir():
     return here / "data"
+
+
+@pytest.fixture()
+def fixture_scorer(fixture_data_dir: Path):
+    documents_pred = [Document.from_knp(path.read_text()) for path in fixture_data_dir.glob("system/*.knp")]
+    documents_gold = [Document.from_knp(path.read_text()) for path in fixture_data_dir.glob("gold/*.knp")]
+
+    cases = ["ガ", "ヲ"]
+    scorer = Scorer(
+        documents_pred,
+        documents_gold,
+        target_cases=cases,
+        exophora_referents=[ExophoraReferent(e) for e in ("著者", "読者", "不特定:人", "不特定:物")],
+        coreference=True,
+        bridging=True,
+        pas_target="all",
+    )
+    return scorer
diff --git a/tests/data/expected/cohesion_score.csv b/tests/data/expected/cohesion_score.csv
new file mode 100644
index 00000000..113ba65c
--- /dev/null
+++ b/tests/data/expected/cohesion_score.csv
@@ -0,0 +1,4 @@
+case,overt,dep,zero_endophora,zero_exophora,zero,dep_zero,all,bridging,coreference
+ガ,0.0,1.0,1.0,0.0,1.0,1.0,1.0
+ヲ,0.0,0.0,1.0,0.0,1.0,1.0,0.666667
+all_case,0.0,1.0,1.0,0.0,1.0,1.0,0.857143,1.0,0.0
diff --git a/tests/data/expected/cohesion_score.txt b/tests/data/expected/cohesion_score.txt
new file mode 100644
index 00000000..55cef0bd
--- /dev/null
+++ b/tests/data/expected/cohesion_score.txt
@@ -0,0 +1,95 @@
+ガ格
+  overt
+    precision: 0.0000 (0/0)
+    recall   : 0.0000 (0/0)
+    F        : 0.0000
+  dep
+    precision: 1.0000 (1/1)
+    recall   : 1.0000 (1/1)
+    F        : 1.0000
+  zero_endophora
+    precision: 1.0000 (1/1)
+    recall   : 1.0000 (1/1)
+    F        : 1.0000
+  zero_exophora
+    precision: 0.0000 (0/0)
+    recall   : 0.0000 (0/0)
+    F        : 0.0000
+  zero
+    precision: 1.0000 (1/1)
+    recall   : 1.0000 (1/1)
+    F        : 1.0000
+  dep_zero
+    precision: 1.0000 (2/2)
+    recall   : 1.0000 (2/2)
+    F        : 1.0000
+  all
+    precision: 1.0000 (2/2)
+    recall   : 1.0000 (2/2)
+    F        : 1.0000
+ヲ格
+  overt
+    precision: 0.0000 (0/0)
+    recall   : 0.0000 (0/1)
+    F        : 0.0000
+  dep
+    precision: 0.0000 (0/0)
+    recall   : 0.0000 (0/0)
+    F        : 0.0000
+  zero_endophora
+    precision: 1.0000 (1/1)
+    recall   : 1.0000 (1/1)
+    F        : 1.0000
+  zero_exophora
+    precision: 0.0000 (0/0)
+    recall   : 0.0000 (0/0)
+    F        : 0.0000
+  zero
+    precision: 1.0000 (1/1)
+    recall   : 1.0000 (1/1)
+    F        : 1.0000
+  dep_zero
+    precision: 1.0000 (1/1)
+    recall   : 1.0000 (1/1)
+    F        : 1.0000
+  all
+    precision: 1.0000 (1/1)
+    recall   : 0.5000 (1/2)
+    F        : 0.6667
+all_case
+  overt
+    precision: 0.0000 (0/0)
+    recall   : 0.0000 (0/1)
+    F        : 0.0000
+  dep
+    precision: 1.0000 (1/1)
+    recall   : 1.0000 (1/1)
+    F        : 1.0000
+  zero_endophora
+    precision: 1.0000 (2/2)
+    recall   : 1.0000 (2/2)
+    F        : 1.0000
+  zero_exophora
+    precision: 0.0000 (0/0)
+    recall   : 0.0000 (0/0)
+    F        : 0.0000
+  zero
+    precision: 1.0000 (2/2)
+    recall   : 1.0000 (2/2)
+    F        : 1.0000
+  dep_zero
+    precision: 1.0000 (3/3)
+    recall   : 1.0000 (3/3)
+    F        : 1.0000
+  all
+    precision: 1.0000 (3/3)
+    recall   : 0.7500 (3/4)
+    F        : 0.8571
+  bridging
+    precision: 1.0000 (1/1)
+    recall   : 1.0000 (1/1)
+    F        : 1.0000
+  coreference
+    precision: 0.0000 (0/2)
+    recall   : 0.0000 (0/1)
+    F        : 0.0000
diff --git a/tests/datamodule/datasets/test_char_dataset.py b/tests/datamodule/datasets/test_char_dataset.py
index dbf2a77f..4ed4e15f 100644
--- a/tests/datamodule/datasets/test_char_dataset.py
+++ b/tests/datamodule/datasets/test_char_dataset.py
@@ -4,7 +4,7 @@
 from rhoknp import Document
 
 from jula.datamodule.datasets.char_dataset import CharDataset
-from jula.utils.utils import ENE_TYPE_BIES, IGNORE_INDEX
+from jula.utils.constants import ENE_TYPE_BIES, IGNORE_INDEX
 
 here = Path(__file__).absolute().parent
 path = here.joinpath("knp_files")
diff --git a/tests/datamodule/datasets/test_word_dataset.py b/tests/datamodule/datasets/test_word_dataset.py
index d8a70930..145d315d 100644
--- a/tests/datamodule/datasets/test_word_dataset.py
+++ b/tests/datamodule/datasets/test_word_dataset.py
@@ -3,15 +3,24 @@
 from pathlib import Path
 
 from rhoknp import Document
+from rhoknp.units.utils import DepType
+from transformers.utils import PaddingStrategy
 
-from jula.datamodule.datasets.word_dataset import WordDataset
-from jula.datamodule.examples import Task
+from jula.datamodule.datasets.word_dataset import WordDataset, WordExampleSet
+from jula.datamodule.examples import (
+    BasePhraseFeatureExample,
+    CohesionExample,
+    DependencyExample,
+    DiscourseExample,
+    Task,
+    WordFeatureExample,
+)
 from jula.datamodule.extractors import PasAnnotation
-from jula.utils.utils import (
+from jula.utils.constants import (
     BASE_PHRASE_FEATURES,
     CONJFORM_TYPES,
     CONJTYPE_TYPES,
-    DEPENDENCY_TYPES,
+    DEPENDENCY_TYPE2INDEX,
     DISCOURSE_RELATIONS,
     IGNORE_INDEX,
     POS_TYPES,
@@ -25,7 +34,7 @@
 
 word_dataset_kwargs = {
     "cases": ["ガ", "ヲ", "ニ", "ガ２"],
-    "bar_rels": ["ノ", "ノ？"],
+    "bar_rels": ["ノ"],
     "exophora_referents": ["著者", "読者", "不特定:人", "不特定:物"],
     "cohesion_tasks": ["pas_analysis", "bridging", "coreference"],
 }
@@ -37,11 +46,7 @@ def test_init():
 
 def test_getitem():
     max_seq_length = 512
-    dataset = WordDataset(
-        str(path),
-        max_seq_length=max_seq_length,
-        **word_dataset_kwargs,
-    )
+    dataset = WordDataset(str(path), max_seq_length=max_seq_length, **word_dataset_kwargs)
     for i in range(len(dataset)):
         document = dataset.documents[i]
         item = dataset[i]
@@ -57,34 +62,27 @@ def test_getitem():
         assert "intra_mask" in item
         assert "dependency_types" in item
         assert "discourse_relations" in item
+        assert "cohesion_target" in item
+        assert "cohesion_mask" in item
         assert item["example_ids"] == i
         assert item["input_ids"].shape == (max_seq_length,)
         assert item["attention_mask"].shape == (max_seq_length,)
         assert item["subword_map"].shape == (max_seq_length, max_seq_length)
-        assert (item["subword_map"].sum(dim=1) != 0).sum() == len(document.morphemes)
+        assert (item["subword_map"].sum(dim=1) != 0).sum() == len(document.morphemes) + dataset.num_special_tokens
         assert item["mrph_types"].shape == (max_seq_length, 4)
         assert item["word_features"].shape == (max_seq_length, len(WORD_FEATURES))
-        assert item["base_phrase_features"].shape == (
-            max_seq_length,
-            len(BASE_PHRASE_FEATURES),
-        )
+        assert item["base_phrase_features"].shape == (max_seq_length, len(BASE_PHRASE_FEATURES))
         assert item["dependencies"].shape == (max_seq_length,)
         assert item["intra_mask"].shape == (max_seq_length, max_seq_length)
         assert item["dependency_types"].shape == (max_seq_length,)
-        assert item["discourse_relations"].shape == (
-            max_seq_length,
-            max_seq_length,
-            len(DISCOURSE_RELATIONS),
-        )
+        assert item["cohesion_target"].shape == (6, max_seq_length, max_seq_length)
+        assert item["cohesion_mask"].shape == (6, max_seq_length, max_seq_length)
+        assert item["discourse_relations"].shape == (max_seq_length, max_seq_length)
 
 
 def test_encode():
     max_seq_length = 512
-    dataset = WordDataset(
-        str(path),
-        max_seq_length=max_seq_length,
-        **word_dataset_kwargs,
-    )
+    dataset = WordDataset(str(path), max_seq_length=max_seq_length, **word_dataset_kwargs)
     document = Document.from_knp(
         textwrap.dedent(
             """\
@@ -94,7 +92,7 @@ def test_encode():
             風 かぜ 風 名詞 6 普通名詞 1 * 0 * 0 "代表表記:風/かぜ カテゴリ:抽象物 漢字読み:訓" <代表表記:風/かぜ><カテゴリ:抽象物><漢字読み:訓><正規化代表表記:風/かぜ><漢字><かな漢字><名詞相当語><文頭><自立><内容語><タグ単位始><文節始><文節主辞>
             が が が 助詞 9 格助詞 1 * 0 * 0 NIL <かな漢字><ひらがな><付属>
             * -1D <BGH:吹く/ふく><文末><補文ト><句点><用言:動><レベル:C><区切:5-5><ID:（文末）><係:文末><提題受:30><主節><格要素><連用要素><動態述語><正規化代表表記:吹く/ふく><主辞代表表記:吹く/ふく>
-            + -1D <BGH:吹く/ふく><文末><補文ト><句点><用言:動><レベル:C><区切:5-5><ID:（文末）><係:文末><提題受:30><主節><格要素><連用要素><動態述語><正規化代表表記:吹く/ふく><主辞代表表記:吹く/ふく><用言代表表記:吹く/ふく><節-区切><節-主辞><時制:非過去><主題格:一人称優位><格関係0:ガ:風><格解析結果:吹く/ふく:動1:ガ/C/風/0/0/000-1;ニ/U/-/-/-/-;ト/U/-/-/-/-;デ/U/-/-/-/-;カラ/U/-/-/-/-;時間/U/-/-/-/-><標準用言代表表記:吹く/ふく>
+            + -1D <BGH:吹く/ふく><文末><補文ト><句点><用言:動><レベル:C><区切:5-5><ID:（文末）><係:文末><提題受:30><主節><格要素><連用要素><動態述語><正規化代表表記:吹く/ふく><主辞代表表記:吹く/ふく><用言代表表記:吹く/ふく><節-区切><節-主辞><時制:非過去><主題格:一人称優位><格関係0:ガ:風><格解析結果:吹く/ふく:動1:ガ/C/風/0/0/000-1;ニ/U/-/-/-/-;ト/U/-/-/-/-;デ/U/-/-/-/-;カラ/U/-/-/-/-;時間/U/-/-/-/-><標準用言代表表記:吹く/ふく><談話関係:000-2/2/条件(順方向)>
             吹く ふく 吹く 動詞 2 * 0 子音動詞カ行 2 基本形 2 "代表表記:吹く/ふく 補文ト" <代表表記:吹く/ふく><補文ト><正規化代表表記:吹く/ふく><かな漢字><活用語><表現文末><自立><内容語><タグ単位始><文節始><文節主辞><用言表記先頭><用言表記末尾><用言意味表記末尾>
             。 。 。 特殊 1 句点 1 * 0 * 0 NIL <英記号><記号><文末><付属>
             EOS
@@ -114,7 +112,37 @@ def test_encode():
             """
         )
     )
-    encoding = dataset.encode(document, dataset.cohesion_examples["000"])
+    words = [morpheme.text for morpheme in document.morphemes]
+    text = " ".join(words)
+    encoding = dataset.tokenizer(
+        words,
+        is_split_into_words=True,
+        padding=PaddingStrategy.MAX_LENGTH,
+        truncation=False,
+        max_length=dataset.max_seq_length - dataset.num_special_tokens,
+    ).encodings[0]
+    word_feature_example = WordFeatureExample()
+    word_feature_example.load(document)
+    base_phrase_feature_example = BasePhraseFeatureExample()
+    base_phrase_feature_example.load(document)
+    cohesion_example = CohesionExample()
+    cohesion_example.load(document, dataset.cohesion_tasks, dataset.extractors)
+    dependency_example = DependencyExample()
+    dependency_example.load(document)
+    discourse_example = DiscourseExample()
+    discourse_example.load(document)
+    example = WordExampleSet(
+        example_id=0,
+        doc_id="",
+        text=text,
+        encoding=encoding,
+        word_feature_example=word_feature_example,
+        base_phrase_feature_example=base_phrase_feature_example,
+        dependency_example=dependency_example,
+        cohesion_example=cohesion_example,
+        discourse_example=discourse_example,
+    )
+    encoding = dataset.encode(example)
 
     mrph_types = [[IGNORE_INDEX] * 4 for _ in range(max_seq_length)]
     # 0: 風
@@ -236,25 +264,32 @@ def test_encode():
 
     dependency_types = [IGNORE_INDEX for _ in range(max_seq_length)]
     # 0: 風 -> 2: 吹く
-    dependency_types[0] = DEPENDENCY_TYPES.index("D")
+    dependency_types[0] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     # 1: が -> 0: 風
-    dependency_types[1] = DEPENDENCY_TYPES.index("D")
+    dependency_types[1] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     # 2: 吹く -> ROOT ("D"として扱う)
-    dependency_types[2] = DEPENDENCY_TYPES.index("D")
+    dependency_types[2] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     # 3: 。 -> 2: 吹く
-    dependency_types[3] = DEPENDENCY_TYPES.index("D")
+    dependency_types[3] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     # 4: すると -> 7: 儲かる
-    dependency_types[4] = DEPENDENCY_TYPES.index("D")
+    dependency_types[4] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     # 5: 桶屋 -> 7: 儲かる
-    dependency_types[5] = DEPENDENCY_TYPES.index("D")
+    dependency_types[5] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     # 6: が -> 5: 桶屋
-    dependency_types[6] = DEPENDENCY_TYPES.index("D")
+    dependency_types[6] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     # 7: 儲かる -> ROOT ("D"として扱う)
-    dependency_types[7] = DEPENDENCY_TYPES.index("D")
+    dependency_types[7] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     # 8: 。 -> 7: 儲かる
-    dependency_types[8] = DEPENDENCY_TYPES.index("D")
+    dependency_types[8] = DEPENDENCY_TYPE2INDEX[DepType.DEPENDENCY]
     assert encoding["dependency_types"].tolist() == dependency_types
 
+    discourse_relations = [[IGNORE_INDEX] * max_seq_length for _ in range(max_seq_length)]
+    discourse_relations[2][2] = DISCOURSE_RELATIONS.index("談話関係なし")
+    discourse_relations[2][7] = DISCOURSE_RELATIONS.index("条件")
+    discourse_relations[7][2] = DISCOURSE_RELATIONS.index("談話関係なし")
+    discourse_relations[7][7] = DISCOURSE_RELATIONS.index("談話関係なし")
+    assert encoding["discourse_relations"].tolist() == discourse_relations
+
 
 def test_pas():
     max_seq_length = 512
@@ -263,7 +298,7 @@ def test_pas():
         max_seq_length=max_seq_length,
         **word_dataset_kwargs,
     )
-    example = dataset.cohesion_examples["w201106-0000060560"]
+    example = [e for e in dataset.examples if e.doc_id == "w201106-0000060560"][0].cohesion_example
     example_expected = json.loads((data_dir / "expected/example/0.json").read_text())
     mrphs_exp = example_expected["mrphs"]
     annotation: PasAnnotation = example.annotations[Task.PAS_ANALYSIS]
diff --git a/tests/evaluators/test_cohesion_scorer.py b/tests/evaluators/test_cohesion_scorer.py
index 6c07a14c..f6984f1e 100644
--- a/tests/evaluators/test_cohesion_scorer.py
+++ b/tests/evaluators/test_cohesion_scorer.py
@@ -1,36 +1,55 @@
+import io
 import json
 from pathlib import Path
 
-from rhoknp import Document
-from rhoknp.rel import ExophoraReferent
-
 from jula.evaluators.cohesion_scorer import Measure, Scorer
 
 
-def test_scorer(fixture_data_dir: Path):
-    documents_pred = [Document.from_knp(path.read_text()) for path in fixture_data_dir.glob("system/*.knp")]
-    documents_gold = [Document.from_knp(path.read_text()) for path in fixture_data_dir.glob("gold/*.knp")]
-
-    with fixture_data_dir.joinpath("expected/cohesion_score.json").open() as f:
-        expected_scores = json.load(f)
-
-    cases = ["ガ", "ヲ"]
-    scorer = Scorer(
-        documents_pred,
-        documents_gold,
-        target_cases=cases,
-        exophora_referents=[ExophoraReferent(e) for e in ("著者", "読者", "不特定:人", "不特定:物")],
-        coreference=True,
-        bridging=True,
-        pas_target="all",
-    )
-
-    result = scorer.run().to_dict()
-    for case in cases:
-        case_result = result[case]
+def test_scorer(fixture_data_dir: Path, fixture_scorer: Scorer):
+    expected_scores = json.loads(fixture_data_dir.joinpath("expected/cohesion_score.json").read_text())
+    score_dict = fixture_scorer.run().to_dict()
+    for case in fixture_scorer.cases:
+        case_result = score_dict[case]
         for anal in Scorer.DEPTYPE2ANALYSIS.values():
             expected: dict = expected_scores[case][anal]
             actual: Measure = case_result[anal]
             assert expected["denom_pred"] == actual.denom_pred
             assert expected["denom_gold"] == actual.denom_gold
             assert expected["correct"] == actual.correct
+
+
+def test_score_result_add(fixture_data_dir: Path, fixture_scorer: Scorer):
+    expected_scores = json.loads(fixture_data_dir.joinpath("expected/cohesion_score.json").read_text())
+    score_result1 = fixture_scorer.run()
+    score_result2 = fixture_scorer.run()
+    score_result = score_result1 + score_result2
+    score_dict = score_result.to_dict()
+
+    for case in fixture_scorer.cases:
+        case_result = score_dict[case]
+        for anal in Scorer.DEPTYPE2ANALYSIS.values():
+            expected: dict = expected_scores[case][anal]
+            actual: Measure = case_result[anal]
+            assert actual.denom_pred == expected["denom_pred"] * 2
+            assert actual.denom_gold == expected["denom_gold"] * 2
+            assert actual.correct == expected["correct"] * 2
+
+
+def test_export_txt(fixture_data_dir: Path, fixture_scorer: Scorer):
+    score_result = fixture_scorer.run()
+    with io.StringIO() as string:
+        score_result.export_txt(string)
+        string_actual = string.getvalue()
+
+    string_expected = fixture_data_dir.joinpath("expected/cohesion_score.txt").read_text()
+    assert string_actual == string_expected
+
+
+def test_export_csv(fixture_data_dir: Path, fixture_scorer: Scorer):
+    score_result = fixture_scorer.run()
+    with io.StringIO() as string:
+        score_result.export_csv(string)
+        string_actual = string.getvalue()
+
+    string_expected = fixture_data_dir.joinpath("expected/cohesion_score.csv").read_text()
+    assert string_actual == string_expected
diff --git a/tests/evaluators/test_dependency_parsing_metric.py b/tests/evaluators/test_dependency_parsing_metric.py
new file mode 100644
index 00000000..d1e076b0
--- /dev/null
+++ b/tests/evaluators/test_dependency_parsing_metric.py
@@ -0,0 +1,234 @@
+import textwrap
+
+import torch
+from rhoknp import Document
+
+from jula.evaluators.dependency_parsing_metric import DependencyParsingMetric
+
+document = Document.from_knp(
+    textwrap.dedent(
+        """\
+        # S-ID:000-1 KNP:5.0-2ad4f6df
+        * 1D <BGH:風/かぜ><文頭><ガ><助詞><体言><一文字漢字><係:ガ格><区切:0-0><格要素><連用要素><正規化代表表記:風/かぜ><主辞代表表記:風/かぜ>
+        + 1D <BGH:風/かぜ><文頭><ガ><助詞><体言><一文字漢字><係:ガ格><区切:0-0><格要素><連用要素><名詞項候補><先行詞候補><正規化代表表記:風/かぜ><主辞代表表記:風/かぜ><解析格:ガ>
+        風 かぜ 風 名詞 6 普通名詞 1 * 0 * 0 "代表表記:風/かぜ カテゴリ:抽象物 漢字読み:訓" <代表表記:風/かぜ><カテゴリ:抽象物><漢字読み:訓><正規化代表表記:風/かぜ><漢字><かな漢字><名詞相当語><文頭><自立><内容語><タグ単位始><文節始><文節主辞>
+        が が が 助詞 9 格助詞 1 * 0 * 0 NIL <かな漢字><ひらがな><付属>
+        * -1D <BGH:吹く/ふく><文末><補文ト><句点><用言:動><レベル:C><区切:5-5><ID:（文末）><係:文末><提題受:30><主節><格要素><連用要素><動態述語><正規化代表表記:吹く/ふく><主辞代表表記:吹く/ふく>
+        + -1D <BGH:吹く/ふく><文末><補文ト><句点><用言:動><レベル:C><区切:5-5><ID:（文末）><係:文末><提題受:30><主節><格要素><連用要素><動態述語><正規化代表表記:吹く/ふく><主辞代表表記:吹く/ふく><用言代表表記:吹く/ふく><節-区切><節-主辞><時制:非過去><主題格:一人称優位><格関係0:ガ:風><格解析結果:吹く/ふく:動1:ガ/C/風/0/0/000-1;ニ/U/-/-/-/-;ト/U/-/-/-/-;デ/U/-/-/-/-;カラ/U/-/-/-/-;時間/U/-/-/-/-><標準用言代表表記:吹く/ふく>
+        吹く ふく 吹く 動詞 2 * 0 子音動詞カ行 2 基本形 2 "代表表記:吹く/ふく 補文ト" <代表表記:吹く/ふく><補文ト><正規化代表表記:吹く/ふく><かな漢字><活用語><表現文末><自立><内容語><タグ単位始><文節始><文節主辞><用言表記先頭><用言表記末尾><用言意味表記末尾>
+        。 。 。 特殊 1 句点 1 * 0 * 0 NIL <英記号><記号><文末><付属>
+        EOS
+        """
+    )
+)
+documents = [document]
+dependency_type_predictions = torch.tensor(
+    [
+        [
+            [1, 1],  # P
+            [0, 0],
+            [0, 0],
+            [0, 0],
+            [0, 0],
+            [0, 0],
+            [0, 0],
+        ]
+    ],
+    dtype=torch.long,
+)
+
+
+def test_dependency_parsing_metric():
+    dependency_parsing_metric = DependencyParsingMetric()
+    dependency_parsing_metric_args = {
+        "example_ids": torch.as_tensor([0]),
+        "dependency_predictions": torch.tensor(
+            [
+                [
+                    [2, 1],  # 風
+                    [0, 2],  # が
+                    [6, 3],  # 吹く
+                    [2, 6],  # 。
+                    [0, 0],
+                    [0, 0],
+                    [0, 0],
+                ]
+            ],
+            dtype=torch.long,
+        ),
+        "dependency_type_predictions": dependency_type_predictions,
+    }
+    dependency_parsing_metric.update(**dependency_parsing_metric_args)
+    for attr in dependency_parsing_metric_args.keys():
+        setattr(
+            dependency_parsing_metric,
+            attr,
+            torch.cat(getattr(dependency_parsing_metric, attr), dim=0),
+        )
+
+    results = dependency_parsing_metric.compute(documents)
+
+    prec, rec = 2 / 2, 2 / 2
+    assert results["base_phrase_UAS_f1"] == 2 * prec * rec / (prec + rec)
+    prec, rec = 4 / 4, 4 / 4
+    assert results["morpheme_UAS_f1"] == 2 * prec * rec / (prec + rec)
+    prec, rec = 1 / 2, 1 / 2
+    assert results["base_phrase_LAS_f1"] == 2 * prec * rec / (prec + rec)
+    prec, rec = 3 / 4, 3 / 4
+    assert results["morpheme_LAS_f1"] == 2 * prec * rec / (prec + rec)
+
+
+def test_cyclic_dependency():
+    dependency_parsing_metric = DependencyParsingMetric()
+    dependency_parsing_metric_args = {
+        "example_ids": torch.as_tensor([0]),
+        "dependency_predictions": torch.tensor(
+            [
+                [
+                    [1, 2],  # 風
+                    [2, 0],  # が
+                    [0, 1],  # 吹く
+                    [2, 6],  # 。
+                    [0, 0],
+                    [0, 0],
+                    [0, 0],
+                ]
+            ],
+            dtype=torch.long,
+        ),
+        "dependency_type_predictions": dependency_type_predictions,
+    }
+    dependency_parsing_metric.update(**dependency_parsing_metric_args)
+    for attr in dependency_parsing_metric_args.keys():
+        setattr(
+            dependency_parsing_metric,
+            attr,
+            torch.cat(getattr(dependency_parsing_metric, attr), dim=0),
+        )
+    """
+    cyclic dependency を解消すると
+    　　　　　  s   g
+    　風が　　 1P  1D
+    　吹く。　-1D -1D
+
+    　　　　  s   g
+    　風　　 1P  2D
+    　が　　 2D  0D
+    　吹く　 3D -1D
+    　。　　-1D  2D
+    となる
+    """
+    results = dependency_parsing_metric.compute(documents)
+
+    prec, rec = 2 / 2, 2 / 2
+    assert results["base_phrase_UAS_f1"] == 2 * prec * rec / (prec + rec)
+    # prec, rec = 0 / 4, 0 / 4
+    assert results["morpheme_UAS_f1"] == 0.0
+    prec, rec = 1 / 2, 1 / 2
+    assert results["base_phrase_LAS_f1"] == 2 * prec * rec / (prec + rec)
+    # prec, rec = 0 / 4, 0 / 4
+    assert results["morpheme_LAS_f1"] == 0.0
+
+
+def test_multiple_roots():
+    dependency_parsing_metric = DependencyParsingMetric()
+    dependency_parsing_metric_args = {
+        "example_ids": torch.as_tensor([0]),
+        "dependency_predictions": torch.tensor(
+            [
+                [
+                    [6, 2],  # 風
+                    [6, 0],  # が
+                    [6, 3],  # 吹く
+                    [6, 2],  # 。
+                    [0, 0],
+                    [0, 0],
+                    [0, 0],
+                ]
+            ],
+            dtype=torch.long,
+        ),
+        "dependency_type_predictions": dependency_type_predictions,
+    }
+    dependency_parsing_metric.update(**dependency_parsing_metric_args)
+    for attr in dependency_parsing_metric_args.keys():
+        setattr(
+            dependency_parsing_metric,
+            attr,
+            torch.cat(getattr(dependency_parsing_metric, attr), dim=0),
+        )
+    """
+    multiple roots を解消すると
+    　　　　　  s   g
+    　風が　　-1P  1D
+    　吹く。　 0D -1D
+
+    　　　　  s   g
+    　風　　-1P  2D
+    　が　　 0D  0D
+    　吹く　 3D -1D
+    　。　　 1D  2D
+    となる
+    """
+    results = dependency_parsing_metric.compute(documents)
+
+    # prec, rec = 0 / 2, 0 / 2
+    assert results["base_phrase_UAS_f1"] == 0.0
+    prec, rec = 1 / 4, 1 / 4
+    assert results["morpheme_UAS_f1"] == 2 * prec * rec / (prec + rec)
+    # prec, rec = 0 / 2, 0 / 2
+    assert results["base_phrase_LAS_f1"] == 0.0
+    prec, rec = 1 / 4, 1 / 4
+    assert results["morpheme_LAS_f1"] == 2 * prec * rec / (prec + rec)
+
+
+def test_no_root():
+    dependency_parsing_metric = DependencyParsingMetric()
+    dependency_parsing_metric_args = {
+        "example_ids": torch.as_tensor([0]),
+        "dependency_predictions": torch.tensor(
+            [
+                [
+                    [2, 1],
+                    [0, 2],
+                    [0, 3],
+                    [2, 0],
+                    [0, 0],
+                    [0, 0],
+                    [0, 0],
+                ]
+            ],
+            dtype=torch.long,
+        ),
+        "dependency_type_predictions": dependency_type_predictions,
+    }
+    dependency_parsing_metric.update(**dependency_parsing_metric_args)
+    for attr in dependency_parsing_metric_args.keys():
+        setattr(
+            dependency_parsing_metric,
+            attr,
+            torch.cat(getattr(dependency_parsing_metric, attr), dim=0),
+        )
+    """
+    no root を解消すると
+    　　　　　  s   g
+    　風が　　 1P  1D
+    　吹く。　-1D -1D
+
+    　　　　 s  g
+    　風　　2P 2D
+    　が　　0D 0D
+    　吹く　3D 6D
+    　。　　6D 2D
+    となる
+    """
+    results = dependency_parsing_metric.compute(documents)
+
+    prec, rec = 2 / 2, 2 / 2
+    assert results["base_phrase_UAS_f1"] == 2 * prec * rec / (prec + rec)
+    prec, rec = 2 / 4, 2 / 4
+    assert results["morpheme_UAS_f1"] == 2 * prec * rec / (prec + rec)
+    prec, rec = 1 / 2, 1 / 2
+    assert results["base_phrase_LAS_f1"] == 2 * prec * rec / (prec + rec)
+    prec, rec = 1 / 4, 1 / 4
+    assert results["morpheme_LAS_f1"] == 2 * prec * rec / (prec + rec)
diff --git a/tests/evaluators/test_phrase_analysis_metric.py b/tests/evaluators/test_phrase_analysis_metric.py
index d9d75fe4..d3508441 100644
--- a/tests/evaluators/test_phrase_analysis_metric.py
+++ b/tests/evaluators/test_phrase_analysis_metric.py
@@ -1,7 +1,7 @@
 import torch
 
 from jula.evaluators.phrase_analysis_metric import PhraseAnalysisMetric
-from jula.utils.utils import BASE_PHRASE_FEATURES, IGNORE_INDEX
+from jula.utils.constants import BASE_PHRASE_FEATURES, IGNORE_INDEX
 
 """  Example
 # S-ID:0-1
@@ -22,8 +22,6 @@
 EOS
 """
 
-phrase_analysis_metric = PhraseAnalysisMetric()
-
 word_feature_predictions = torch.tensor(
     [
         [
@@ -61,54 +59,12 @@
             [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # 図書
             # 館 (<節-主辞>と誤って予測)
             [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
-            [
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-            ],  # に (IGNORED)
+            # に (IGNORE)
+            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
             # 行こう (<モダリティ-命令>と誤って予測)
             [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
-            [
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-                0,
-            ],  # 。 (IGNORED)
+            # 。 (IGNORED)
+            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
         ]
     ],
     dtype=torch.long,
@@ -128,6 +84,7 @@
     dtype=torch.long,
 )
 
+phrase_analysis_metric = PhraseAnalysisMetric()
 phrase_analysis_metric_args = {
     "example_ids": torch.as_tensor([0]),
     "word_feature_predictions": word_feature_predictions,
@@ -135,7 +92,6 @@
     "base_phrase_feature_predictions": base_phrase_feature_predictions,
     "base_phrase_features": base_phrase_features,
 }
-
 phrase_analysis_metric.update(**phrase_analysis_metric_args)
 for attr in phrase_analysis_metric_args.keys():
     setattr(
@@ -143,7 +99,6 @@
         attr,
         torch.cat(getattr(phrase_analysis_metric, attr), dim=0),
     )
-
 results = phrase_analysis_metric.compute()
 
 
@@ -163,7 +118,7 @@ def test_phrase_analysis_metric():
     assert results["体言_f1"] == 2 * prec * rec / (prec + rec)
     prec, rec = 1 / 1, 1 / 1
     assert results["モダリティ-意志_f1"] == 2 * prec * rec / (prec + rec)
-    # prec, rec = 0., 0 / 1
+    # prec, rec = 0 / 0, 0 / 1
     assert results["モダリティ-勧誘_f1"] == 0.0
     prec, rec = 1 / 2, 1 / 1
     assert results["節-主辞_f1"] == 2 * prec * rec / (prec + rec)