# Projet dans le cadre du cours de "Méthodes en apprentissage automatique" du master PluriTAL
> BOTTERO Noélie & PHOMMADY Elodie
## Objectif du projet
À travers ce projet, nous souhaitons réaliser un analyseur morphologique du japonais qui réalise une segmentation en mots et un étiquetage en parties du discours (POS).
## Données
### Corpus UD Japanese-GSD
Pour ce projet, nous utilisons un corpus en libre accès mis à disposition par Universal Dependencies : le corpus *Japanese-GSD*. Il contient environ 16 000 phrases tirées de Google (Google Universal Dependency Treebanks v2.0).
Ce corpus est très bien organisé. Il est déjà séparé en sous-corpus d'entrainement, de validation et de test. Les données sont au format CoNLL-U et texte brut.
*Exemple d'un fichier du corpus*
```conllu
# newdoc id = train-s1
# sent_id = train-s1
# text = ホッケーにはデンジャラスプレーの反則があるので、膝より上にボールを浮かすことは基本的に反則になるが、その例外の一つがこのスクープである。
1 ホッケー ホッケー NOUN 名詞-普通名詞-一般 _ 9 obl _ BunsetuBILabel=B|BunsetuPositionType=SEM_HEAD|LUWBILabel=B|LUWPOS=名詞-普通名詞-一般|SpaceAfter=No
2 に に ADP 助詞-格助詞 _ 1 case _ BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|LUWBILabel=B|LUWPOS=助詞-格助詞|SpaceAfter=No
3 は は ADP 助詞-係助詞 _ 1 case _ BunsetuBILabel=I|BunsetuPositionType=FUNC|LUWBILabel=B|LUWPOS=助詞-係助詞|SpaceAfter=No
4 デンジャラス デンジャラス NOUN 名詞-普通名詞-一般 _ 5 compound _ BunsetuBILabel=B|BunsetuPositionType=CONT|LUWBILabel=B|LUWPOS=名詞-普通名詞-一般|SpaceAfter=No
...
```
Il faut savoir qu'en japonais, on recense trois couches de tokénisation :
- Short Unit Word (SUW),
- Long Unit Word (LUW),
- et base-phrase (bunsetsu).
Dans notre projet, nous choisissons de réaliser une segmentation sur la base du Short Unit Word, tel qu'il est proposé dans le corpus UD. Les Short Unit Words sont représentés dans la seconde colonne du tabulaire.
### Pré-traitement des données
Le corpus *Japanese-GSD* est bien constitué et est très complet. Néanmoins, pour notre analyseur morphologique, il y a énormément d'informations dont nous faisons abstraction. De plus, nous avons choisi de traiter les phrases caractère par caractère, auquel nous attribuons une étiquette sous le schéma BIO (plus précisément IOB2), accompagnée de la partie du discours du token dans lequel ce caractère appartient.
Nous avons donc choisi de passer par une phase de prétraitement pour ne récupérer que les informations qui nous intéressent au préalable et éviter que le DatasetReader soit difficile à comprendre et à prendre en main. Nous voulons tout de même garder les lignes de métadonnées et celle avec les phrases entières.
Nous faisons le choix de représenter la segmentation des tokens dans la phrase avec une annotation BIO, en précisant que nous utiliserons seulement les étiquettes B (*beginning*) et I (*inside*). Nous faisons le choix de ne pas utiliser le schéma d'annotation BIOES, et par conséquent l'étiquette S (*single element*). Les tokens à un seul caractère seront donc étiquetés d'un B.
Pour les parties du discours, nous utilisons les annotations XPOS du corpus *Japanese-GSD*, qui correspondent aux parties du discours des Short Unit Words, sur la base du *UniDic POS tagset*.
Suite aux prétraitements, nos données sont stockées dans le répertoire *Corpus_Analyzer* et sont au format suivant :
```text
# newdoc id = train-s1
# sent_id = train-s1
# text = ホッケーにはデンジャラスプレーの反則があるので、膝より上にボールを浮かすことは基本的に反則になるが、その例外の一つがこのスクープである。
1 ホ B-NOUN
2 ッ I-NOUN
3 ケ I-NOUN
4 ー I-NOUN
5 に B-ADP
6 は B-ADP
...
```
## DatasetReader
Dans notre DatasetReader, la création d'une instance se fait à l'échelle d'une phrase. Une instance correspond à un TextField contenant les caractères de la phrase en japonais et à un SequenceLabelField contenant les étiquettes BIO (avec les POS) de chaque caractère de la phrase.
Pour le *TextField*, nous avons choisi des tokens qui sont des unigrammes. Grâce à cela, lorsque nous lisons le fichier de données, nous pouvons directement utiliser les caractères dans la partie tabulaire qui sont déjà divisés en unigrammes et également réaliser la récupération des étiquettes pour le *SequenceLabelField*.
## Baseline
### Modèle utilisé
Après avoir implémenté le *DatasetReader*, nous avons programmé le modèle *baseline*, qui est un modèle construit dans notre cas de manière naïve et que nous améliorerons par la suite.
Cette *baseline* est très similaire aux modèles que nous avions réalisés en cours.
La classe qui la représente est composée de trois méthodes :
- un constructeur `__init__`, dans lequel nous précisons la nature des éléments utilisés dans le modèle : l'embedder, l'encoder, le feed forward,...
- une fonction `forward` qui établit le parcours des données au sein du modèle
- une fonction `get_metrics` qui permet d'avoir des scores, dans notre cas les scores d'exactitude, au fur et à mesure de l'entrainement
Dans un premier temps, le modèle a besoin de transformer la séquence brute de tokens donnée en entrée en séquence de vecteurs : des embeddings. Nous avons choisi un embedder simple pour réaliser cette tâche : *BasicTextFieldEmbedder*.
Ensuite, il faut aplatir ces séquences de vecteurs avec un encoder. Sachant que nos instances sont composées d'une séquence de tokens et d'une séquence de labels, nous devions choisir des encoders *seq2seq* qui prennent une séquence en entrée et renvoient une séquence en sortie.
Nous avons décidé de tester pour le moment trois modèles "simples" d'encoder : un RNN, un GRU et un LSTM. Le RNN permet notamment de garder la notion d'ordre entre les tokens et capture le côté linéaire du texte. Pour la baseline, nous avons entrainé le modèle d'apprentissage avec une représentation unidirectionnelle du texte.
Nous devons ensuite procéder à l'annotation des labels pour les différents caractères. Pour cela, nous utilisons un FeedForward qui a plusieurs paramètres :
- la taille des vecteurs en entrée, qui correspond à la taille des vecteurs sortant de l'encoder,
- le nombre de couche, que nous avons déterminé à 1,
- le nombre de "classes", qui correspond en fait au nombre de labels et nous pouvons l'obtenir en récupérant le nombre de labels différents du vocabulaire avec `vocab.get_vocab_size("labels")`,
- la fonction d'activation, qui est dans notre cas *ReLU* (Rectified Linear Unit).
Sachant qu'en sortie, nous ne voulons pas seulement un label mais une séquence ordonnée de labels, il faut utiliser TimeDistributed avec le FeedForward.
Et enfin, pour que nous puissions avoir un résultat d'exactitude pendant l'entraînement, nous utilisons la mesure *CategoricalAccuracy* qui repose sur la notion de *Top-K accuracy* (K étant un nombre égal à 1) et la fonction `get_metrics` qui met à jour les scores d'exactitude au fur et à mesure de l'entraînement. Pour calculer la fonction d'erreur sur une séquence, on utilise aussi *sequence_cross_entropy_with_logits*.
### Résultats
| | RnnSeq2SeqEncoder | GruSeq2SeqEncoder | LstmSeq2SeqEncoder |
| :----------------------- | :---------------: | :---------------: | :----------------: |
| best_epoch | 49 | 49 | 49 |
| epoch | 49 | 49 | 49 |
| training_accuracy | 0.382 | 0.210 | 0.393 |
| training_loss | 2.591 | 2.697 | 2.756 |
| validation_accuracy | 0.373 | 0.219 | 0.375 |
| validation_loss | 2.590 | 2.704 | 2.745 |
| best_validation_accuracy | 0.373 | 0.219 | 0.375 |
| best_validation_loss | 2.590 | 2.704 | 2.745 |
<img src="img/test_baseline_RNN.png" alt="RNN_baseline.png" style="zoom: 67%;" />
<img src="img/test_baseline_GRU.png" alt="GRU_baseline.png" style="zoom: 67%;" />
<img src="img/test_baseline_LSTM.png" alt="LSTM_baseline.png" style="zoom:67%;" />
### Observations
Notre baseline repose sur un modèle naïf, qui nous sert à confirmer que le reader et le modèle fonctionnent. Pour être tout à fait honnêtes, nous avions utilisé pour la baseline des valeurs de paramètres récupérées d'exemples en ligne, sans vraiment les adapter à notre modèle. Avec les résultats obtenus, nous pouvons voir que celles-ci ne conviennent pas du tout à notre modèle.
Dans un premier temps, le nombre d'instance maximal est beaucoup trop faible comparé à la taille des batchs saisie. Le modèle ne peut pas bien apprendre avec seulement 500 instances sachant que les batchs sont de taille 128. De plus, utiliser uniquement 500 instances signifie que nous n'utilisons qu'une petite partie des données que nous avons.
Ensuite, on remarque que les résultats ne sont pas très bons. Mais là encore, les paramètres n'étaient pas adaptés à notre modèle. Comme nous pouvons le voir dans le tableau et les graphiques (obtenus avec le script *graph.py* dans le dossier `evals`), le nombre d'epochs limite l'apprentissage du modèle, qui semble ne pas avoir fini d'apprendre. Les meilleurs résultats sont obtenus à chaque fois à la dernière epoch (49) et la courbe descendante montre bien que l'apprentissage n'est pas fini. De plus, les résultats de *loss* des données d'apprentissage et des données de validation sont extrêmement proches, ce qui est relativement étrange.
Enfin, nous avions retenu trois types d'encoders : RNN, GRU et LSTM. Comparé au RNN et au LSTM, le GRU a une *accuracy* plus faible, mais sa *loss* reste dans la même échelle. On s'aperçoit aussi que le RNN et le LSTM obtiennent des résultats très similaires. Le LSTM est plus exact, mais le RNN a un taux d'erreur plus faible. Les résultats ne se démarquant pas vraiment les uns des autres, nous allons continuer à tester les trois encoders, jusqu'à qu'un nous donne des résultats notablement meilleurs.
Après avoir analysé brièvement ces résultats, nous avons eu quelques idées d'améliorations.
- utiliser des embeddings plus grand,
- utiliser des embeddings pré-entraînés (en faisant attention que la tokénisation soit la même que celle que nous avons réalisée),
- augmenter le nombre d'instance,
- réduire la taille du batch,
- augmenter le nombre d'epochs,
- utiliser à bon escient la patience pour que l'apprentissage s'arrête quand le modèle n'apprend plus sur un nombre déterminé d'epochs,
- tester des transformers.
## Améliorations
### 1. Changer manuellement les hyperparamètres du modèle
#### Modèle utilisé
L'élément qui diffère du modèle de base est la possibilité de lire le texte de manière
unidirectionnelle ou bidirectionnelle. Mis à part cela, le modèle en lui-même n'a pas été changé. Ce qui a le plus changé, ce sont les hyperparamètres de l'entraînement dans le fichier de configuration.
Les hyperparamètres sont les "paramètres" que l'utilisateur définit, le modèle n'a pas de moyen de les modifier. Il s'avère que ces hyperparamètres peuvent avoir une influence considérable sur la qualité de l'entraînement du modèle. Comme nous l'avons aperçu dans les résultats de la baseline, des hyperparamètres non adaptés engendrent un mauvais apprentissage.
Ici, nous essayons des hyperparamètres qui coïncident plus avec notre modèle.
Parmi les changements d'hyperparamètres, nous avons :
- augmenté le nombre d'instances, pour qu'il couvre l'intégralité des données,
- augmenté la taille des embeddings,
- réduit la taille du batch,
- augmenté le nombre d'epochs.
#### Résultats pour les versions avec des encoders unidirectionnels
| | RnnSeq2SeqEncoder *(training stopped : ran out of patience)* | GruSeq2SeqEncoder *(training stopped : ran out of patience)* | LstmSeq2SeqEncoder *(training stopped : ran out of patience)* |
| :----------------------- | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
| best_epoch | 10 | 16 | 12 |
| epoch | 20 | 26 | 22 |
| training_accuracy | 0.888 | 0.716 | 0.682 |
| training_loss | 0.309 | 0.985 | 1.561 |
| validation_accuracy | 0.842 | 0.667 | 0.660 |
| validation_loss | 0.513 | 1.266 | 1.714 |
| best_validation_accuracy | 0.841 | 0.671 | 0.643 |
| best_validation_loss | 0.490 | 1.209 | 1.691 |
<img src="img/test_amelioration_1_unidirectional_RNN.png" alt="RNN amélioré unidirectionnel" style="zoom: 67%;" />
<img src="img/test_amelioration_1_unidirectional_GRU.png" alt="GRU amélioré unidirectionnel" style="zoom: 67%;" />
<img src="img/test_amelioration_1_unidirectional_LSTM.png" alt="LSTM amélioré unidirectionnel" style="zoom: 67%;" />
#### Résultats pour le RNN bidirectionnel
| | RnnSeq2SeqEncoder *(training stopped : ran out of patience)* |
| :----------------------- | :----------------------------------------------------------: |
| best_epoch | 8 |
| epoch | 18 |
| training_accuracy | 0.930 |
| training_loss | 0.221 |
| validation_accuracy | 0.876 |
| validation_loss | 0.454 |
| best_validation_accuracy | 0.876 |
| best_validation_loss | 0.411 |
<img src="img/test_amelioration_1_bidirectional_RNN.png" alt="RNN amélioré bidirectionnel" style="zoom: 67%;" />
#### Observations
Après avoir modifié les paramètres, nous observons que les résultats sont largement meilleurs que ceux de la baseline et que les scores de l'entraînement et de la validation ne sont plus semblables, comme pour les expériences précédentes. Cela confirme le fait que les paramètres n'étaient pas du tout adaptés.
Sur la version modifiée, un écart se creuse entre les résultats du RNN et ceux du GRU et LSTM. L'exactitude augmente de deux points et la *loss* est notablement réduite. En s'appuyant sur ces observations, nous avons décidé de ne travailler que sur le RNN dès à présent.
Nous avons par la suite testé le RNN bidirectionnel, qui nous donne des meilleurs résultats en moins de temps et de répétitions. L'apprentissage est légèrement optimisé lorsque nous lisons le texte de manière bidirectionnelle.
### 2. Optimiser les hyperparamètres du modèle
Tel que précédemment, l'apprentissage peut dépendre énormément des hyperparamètres du modèle. Avec un optimisateur d'hyperparamètres comme Optuna, nous pouvons réaliser des tests sur notre modèle pour voir quelle est la combinaison d'hyperparamètres qui donne les meilleurs résultats.
#### Résultats
Au plus le nombre d'hyperparamètres est optimisé, au plus le nombre de combinaisons possibles est grand. Compte tenu du nombre assez restreint d'essais obtenus avec un *timeout* de 18000 (correspondant à cinq heures), nous avons fait le choix de ne sélectionner que quatre hyperparamètres pour limiter la combinatoire. Nous n'avons pas pu tester un *timeout* plus élevé sur nos machines, car le processus était relativement long. Nous avons essayé de le déployer sur le serveur distant mis à notre disposition par l'équipe ERTIM. Néanmoins, nous avons eu quelques difficultés à installer à faire fonctionner les scripts (problèmes de compatibilité entre CUDA et PyTorch).

<img src="img/optuna_accuracy_graph.png" alt="image-20220522201809177" style="zoom:67%;" />
<img src="img/optuna_accuracy_importances.png" alt="image-20220522201916116" style="zoom: 62%;" />
En considérant le score d'exactitude maximum, nous avons obtenu sept essais au total. Si on prend un peu de recul, on souligne qu'il y a une plus grande marge de progression à faire sur la *loss* que sur l'*accuracy*. Le score d'exactitude était déjà assez élevée : 0,876. Il est plus difficile de trouver la combinaison des hyperparamètres qui permettrait d'augmenter l'exactitude que de trouver celle qui fera baisser la *loss*, qui est de 0,411.
Par conséquent, nous avons aussi lancé une optimisation des hyperparamètres en considérant la plus petite *loss*. Néanmoins, en plus de cinq heures d'exécution, un seul essai a été complété. Nous décidons donc de ne conserver que les résultats de la maximisation de l'*accuracy*.
#### Observations
Résultats des améliorations avec changement d'hyperparamètres :
| | *Config 1*<br/>dim: 36<br/>batch:4 | *Config 2*<br/>dim:86<br/>batch:7 | *Config 4*<br/>dim:41<br/>batch:10 | *Config 6*<br/>dim:95<br/>batch:9 |
|:-------------------------:|:--------------------------------------------------------------:|:-----------------------------:|:----------------------------------:|:---------------------------------:|
| best_epoch | 3 | 26 | 106 | 106 |
| epoch | 13 | 36 | 116 | 116 |
| training_accuracy | 0.847 | 0.939 | 0.908 | 0.913 |
| training_loss | 0.475 | 0.159 | 0.274 | 0.259 |
| validation_accuracy | 0.837 | 0.873 | 0.882 | 0.887 |
| validation_loss | 0.529 | 0.435 | 0.392 | 0.382 |
| best_validation_accuracy | 0.849 | 0.898 | 0.883 | 0.886 |
| best_validation_loss | 0.490 | 0.334 | 0.388 | 0.379 |
Nous avons conservé les meilleurs essais parmi les sept lancés. Nous pouvons voir que plus le nombre de dimensions augmente, plus le résultat s'améliore. La taille du batch semble donner de meilleurs résultats si elle est située entre sept et dix. L'optimiseur sgd est utilisé pour toutes les configurations sauf la première, il pourrait également être un facteur d'amélioration.
**Courbe de perte et de précision du meilleur modèle**

### 3. Utiliser des embeddings pré-entrainés
Nous avons ensuite choisi d'utiliser des embeddings pré-entraînés qui permettraient d'améliorer notre modèle déjà existant, en se basant sur le meilleur modèle issu des précédentes expériences. Nous avons sélectionné des vecteurs sur le site Fasttext qui est une bibliothèque où l'on peut trouver des embeddings pré-entraînés dans la langue de son choix. Pour le japonais, nous avons donc pris le suivant : https://fasttext.cc/docs/en/crawl-vectors.html
Les paramètres du meilleur modèle ont été conservés afin d'entraîner le nouveau avec des embeddings pré-entraînés. Cependant, nous avons conservé les dimensions des vecteurs de Fasttext, qui sont au nombre de 300. Nous pensons qu'en diminuant le nombre dimension, l'utilisation d'embeddings pre-entraînés seraient moins pertinente.
#### Observations
Suite à cette expérience, les résultats restent légèrement les mêmes. Les embeddings pré-entraînés n'ont donc pas amélioré nos résultats.
### 4. Utilisation de transformers
Les embeddings pré-entraînés n'ayant pas donné de meilleurs résultats, nous nous sommes ensuite tourné vers les transformers. Ces algorithmes sont très lourds, mais ce sont les plus efficaces.
Transformer utilisé : BeRT (Cl Tohoku : bert-base-japanese-char)
Cependant, nous ne sommes pas parvenus à appliquer les transformers à notre modèle. La méthode forward doit être modifiée pour fonctionner correctement. Son premier argument est un *LongTensor*, et le deuxième argument un *mask*. Voici donc à quoi la fonction forward devrait ressembler :
``` python
class PretrainedTransformerEmbedder(TokenEmbedder):
| ...
| def forward(
| self,
| token_ids: torch.LongTensor,
| mask: torch.BoolTensor,
| type_ids: Optional[torch.LongTensor] = None,
| segment_concat_mask: Optional[torch.BoolTensor] = None
| ) -> torch.Tensor
```
## Récapitulatif des modèles et des scores obtenus :
1. Baseline
2. RNN Unidirectionnel
3. RNN Bidirectionnel
4. RNN Bidirectionnel avec hyper paramètres modifiés. **(Meilleur modèle)**
5. Embeddings pre-entraînés
6. Modèle n°4 avec ajout de couches supplémentaires
| | 1 | 2 | 3 | 4 | 5 |6|
| :----------------------- |:-----:|:-----:|:-----:|:---:|:---:|:----:|
| best_epoch | 49 | 10 | 8 | 26 |26|16|
| epoch | 49 | 20 | 18 | 36 |36|26|
| training_accuracy | 0.382 | 0.888 | 0.930 |0.939|0.893|0.895|
| training_loss | 2.591 | 0.309 | 0.221 |0.159|0.274|0.343|
| validation_accuracy | 0.373 | 0.842 | 0.876 |0.873|0.835|0.868|
| validation_loss | 2.590 | 0.513 | 0.454 |0.435|0.520|0.476|
| best_validation_accuracy | 0.373 | 0.841 | 0.876 |0.898|0.841|0.867|
| best_validation_loss | 2.590 | 0.490 | 0.411 |0.334|0.479|0.475|
Le quatrième modèle, avec RNN bidirectionnel et les hyperparamètres optimisés, nous donne la meilleure *accuracy* et la plus faible *loss*, même s'il requiert plus d'epochs pour l'apprentissage. Nous considérons ce modèle comme le meilleur et nous l'utilisons donc pour traiter les données de test.
## Application du meilleur modèle sur les données de test
### Evaluation
Maintenant que nous avons déterminé quel est le meilleur modèle, nous pouvons lancer l'évaluation du modèle sur les données de test.
Et les résultats sur les données de test sont les suivants :
```json
{
"accuracy": 0.9301944713992489,
"loss": 0.19043113660466457
}
```
Ce qui est intéressant et assez surprenant c'est que nous obtenons des meilleurs scores que sur les données de validation.
Nous ne perdons pas de vue que le modèle a appris de manière simultanée les frontières de mots et les parties de discours. Les résultats de *loss* et *accuracy* ne dépendent pas uniquement d'une bonne ou mauvaise tokénisation, ni d'une bonne ou mauvaise analyse syntaxique. Ce sont les résultats des deux tâches conjointes.
### Prédiction
Nous voulons également pouvoir récupérer les prédictions du modèle. Ainsi, nous pouvons transposer les résultats pour rendre les phrases plus compréhensibles pour un humain et analyser la sortie du modèle.
Il a fallu modifier légèrement le `forward` du modèle et ajouter un prédicteur pour qu'il puisse non seulement nous donner les scores d'erreurs pour chaque instance, mais aussi les prédictions qu'il avait fait.
Nous remarquons dans la sortie de cette commande que les caractères japonais ne sont pas représentés en tant que tel mais en tant que code Unicode (*ex.* `"\u306b"`).
## Post-traitement
Avec les résultats du modèle, il faut pouvoir reconstruire une phrase tokénisé dont les tokens sont séparés par des espaces et sont annotés avec leur partie du discours.
Nous avons donc parsé le fichier JSON de prédictions et ré-écrit les phrases issues de la prédiction du modèle et celles avec les annotations de base dans deux fichiers distincts : `res_pred_sentences.txt` et `res_ref_sentences.txt`.
Concernant le problème énoncé précédemment avec les caractères représentés par leurs codes Unicode, nous avons pu le régler en précisant l'encodage (`encoding="utf-8"`) lors de l'écriture des fichiers de sorties.
*Exemple de phrase*
```text
室長/NOUN の/ADP 対応/NOUN に/ADP は/ADP 終始/NOUN 誠実/NOUN さ/PART が/ADP 感じ/VERB られ/AUX た/AUX
```
Traduction : "*Le directeur a été sincère dans ses réponses tout au long du processus.*"
Dans cette phrase exemple, seul le token 終始 n'a pas été correctement annoté (NOUN au lieu de ADV). Cependant, ce mot peut être à la fois un adverbe ou un nom en fonction du contexte.
Nous pouvons faire une comparaison rapide avec la commande `diff` pour voir quelles phrases diffèrent. Mais nous n'avons pas eu le temps d'implémenter un programme permettant de faire une comparaison plus précise sur les tokens mal segmentés ou mal annotés.
## Conclusion
La tokenisation et le pos-tagging du japonais est une tâche encore peu utilisée avec des architectures de machine learning. Il fut donc difficile de trouver des travaux à ce sujet, afin de nous en inspirer et de les améliorer. Nous avons décidé de commencer par un modèle très simple et de le complexifier au fur et à mesure. Notre modèle le plus performant n'était pas celui auquel nous nous attendions. Nous pensions que les embeddings pré-entraînés allaient drastiquement améliorer les résultats. Afin d'améliorer les résultats, il serait tout à fait pertinent de continuer sur la mise en place d'un modèle à base de Transformer.
## Bibliographie
Izutsu, J., & Komiya, K. *Morphological Analysis of Japanese Hiragana Sentences using the BI-LSTM CRF Model*. 2021, ArXiv, abs/2201.03366
Morita, H., Kawahara, D., & Kurohashi, S. *Morphological Analysis for Unsegmented Languages using Recurrent Neural Network Language Model*. 2015, EMNLP.
Taku, K., Kaoru, Y., Yuji, M. *Applying Conditional Random Fields to Japanese Morphological Analysis*. 2004, EMNLP.
Tolmachev, A., Kawahara, D., & Kurohashi, S. *Design and Structure of The Juman++ Morphological Analyzer Toolkit*. 2020.