a3nm's blog

Haspirater -- identifying initial aspirated 'h's in French words

— updated

English version

Version française ci-dessous.

I just wrote haspirater, a system to detect if the initial 'h' in a French word is aspirated or not. (I happened to need this, and no one had apparently done it yet.) For those who are unfamiliar with the context, the thing is that for French words which start with an 'h', the 'h' can be aspirated or non aspirated, which changes the behavior of the word regarding elision and liaison. Of course, there are no known rules to find out from the structure of a word whether it is aspirated or not...

The simple approach would be to use word lists, but of course they are never complete and will fail for unseen words. A natural solution for them would be to assume that they behave in the same way that the closest known word. This forces us to define "closest", and it seems reasonable to look at the word with the longest common prefix, because it looks like the property of whether the 'h' is aspired or not should be mostly conditioned by the beginning of the word. (Actually, it would be better to look at the pronunciation of the beginning of the word if we could afford it).

This suggests a simple optimization. If we are going to take the result of the closest word in this fashion, we might as well drop words from the known words list which do not contribute anything to the result. In other words, if all words starting with "hach" in the list have an aspirated 'h', then there is no need to store them all; just storing "hach" will be enough. In fact, this means that the appropriate structure for our word list is a trie, and the optimization that I mentioned is apparently called compression.

Another trick is that we can try to infer word lists automatically from a corpus using some simple rules. If we read "la hache" in a text, it means that the initial 'h' is aspirated, whereas "l'hirondelle" indicates that "hirondelle" starts with a non-aspirated 'h'. We can easily process megabytes of text and get word lists in this way.

This approach works quite well, yielding a dataset which is under 5 KB in json (and less than 1 KB compressed) and a lookup program which is just 40 lines of Python. It gives the right result for any example I could come up with (though I had to add a few exceptions for words missing from the corpus, both manually and using Wikipedia and Wiktionary); hopefully it will also work for you, please tell me if it doesn't. Note that we could even compile the trie into a very efficient C program if speed was of the essence.

Another amusing thing is that we can draw the data and try to see what the "rules" are. Here is the trie (it is quite messy). The node border and edge thickness are are a logarithmic function of the number of occurrences, the node labels indicate the prefix, and the color is red for aspirated h and blue for non-aspirated (or a mix thereof for ambiguous cases, depending on the proportion).

The Wikipedia article I linked above mentions that this approach is used to store Unicode character properties efficiently; it seems like it could be used for a lot of other things. For instance, you could imagine the trie indicating the gender of nouns (though you would probably build it on suffixes rather than prefixes in this case), the trie of prepositions used for a given noun (say "в" vs "на" in Russian), and probably tries for a lot of other arbitrary things that are so obvious to native speakers.

Version française

English version above.

Je viens d'écrire haspirater, un système pour détecter les 'h' aspirés au début des mots français. (Il se trouve que j'en avais besoin, et, apparemment, personne ne l'avait encore fait.) Pour ceux qui ne connaissent pas le contexte, le problème est que pour les mots français qui commencent par un 'h', le 'h' peut être aspiré ou non aspiré (ce qui ne change rien à la prononciation, mais change le fonctionnement de l'élision et de la liaison). Évidemment, il n'y a pas de règles pour déterminer à partir de la structure d'un mot si le 'h' initial est aspiré ou non...

L'approche la plus simple serait d'utiliser une liste de mots, mais évidemment de telles listes ne sont jamais complètes et ne fonctionneront pas pour des mots inconnus. Une solution naturelle pour ces derniers serait de supposer qu'ils se comportent de la même manière que le mot connu le plus proche. Cela nous oblige à définir "proche" : il paraît raisonnable de regarder le mot ayant le plus long préfixe commun avec le mot inconnu, parce qu'on a tendance à se dire que c'est le début du mot qui a le plus d'influence sur le fait que le 'h' soit aspiré ou non. (En fait, ce serait plutôt la prononciation du début du mot qu'il faudrait regarder si on la connaissait, mais passons...)

Cela nous mène à une optimisation simple. Si on prend toujours le résultat du mot le plus proche de la façon décrite plus haut, on peut très bien retirer de la liste de mots connus ceux qui ne sont pas nécessaires pour déduire le résultat. En d'autres termes, si tous les mots qui commencent par "hach" dans la liste ont un 'h' aspiré, alors il n'est pas nécessaire de les stocker tous, il suffit de stocker "hach". En fait, cela veut dire que la bonne structure pour notre liste de mots est un trie, et l'optimisation que je viens de décrire s'appelle apparemment la compression.

Une autre astuce est qu'on peut essayer d'inférer automatiquement des listes de mots à partir d'un corpus en utilisant quelques règles simples. Si on lit "la hache" dans un texte, cela signifie que le 'h' initial est aspiré, alors que "l'hirondelle" indique qu'"hirondelle" commence par un 'h' non aspiré. On peut facilement traiter plusieurs mégaoctets de texte pour en tirer des listes de mots de cette façon.

Cette approche fonctionne plutôt bien : le fichier de données fait moins de 5 Ko en json (et moins de 1 Ko compressé), et le programme pour chercher un mot fait 40 lignes de Python. Il donne le bon résultat pour tous les exemples que j'ai essayés (quoique j'ai dû ajouter à la main quelques exceptions pour des mots absents du corpus, en utilisant les listes de Wikipédia et du Wiktionnaire) ; j'espère qu'il marchera aussi pour vous, et vous prie de me signaler toute erreur. Notez qu'on pourrait même compiler le trie en un programme en C très efficace si la vitesse était importante.

Un autre truc amusant qu'on peut faire, c'est faire un dessin des données et voir quelles semblent être les "règles". Voici le trie (il est plutôt bordélique). L'épaisseur du bord des nœuds et des arêtes est en fonction du nombre d'occurrences (logarithmiquement), les étiquettes des nœuds indiquent le préfixe, les nœuds en rouge indiquent l'aspiration, les nœuds en bleu indiquent la non-aspiration, et un mélange indique les cas ambigus (en fonction de la proportion).

L'article Wikipédia cité plus haut indique que cette approche est utilisée pour un stockage efficace des propriétés des caractères Unicode ; j'ai l'impression qu'elle pourrait être utilisée pour tout un tas d'autres choses. On pourrait imaginer, par exemple, un trie du genre des noms (quoiqu'il serait probablement préférable de regarder les suffixes plutôt que les préfixes dans ce cas), un trie du choix des propositions en fonction du nom ("в" vs "на" en russe, entre autres), et sans doute des tries pour un tas d'autres choses arbitraires qui sont tellement évidentes pour les locuteurs natifs.

comments welcome at a3nm<REMOVETHIS>@a3nm.net