ChemDataExtractor：シンプルテキストから固有表現抽出（Named Entity Recognition; NER）を行ってみる

# 概要

論文や特許文献から材料名，化合物名，そしてそれに紐づく物性値を自動的に取得したり抽出したりしてマイニングしたい．そのようなときに使われるのが，近年ではpythonライブラリのChemDataExtractorに勢いがあります．あまり日本語の解説サイトがないので，メモとして書き残しておきます．


* [ChemDataExtractor（導入編）](https://qiita.com/shin0502/items/a9904c4509b9b08a5c10)


# テキスト
今回のテキスト解析はオープンジャーナルの*Nanomaterials*から，以下の有機ELの青色発光のTADF論文から例文を使います．

*Nanomaterials* **2019**, 9(12), 1735; https://doi.org/10.3390/nano9121735
**A Novel Design Strategy for Suppressing Efficiency Roll-Off of Blue Thermally Activated Delayed Fluorescence Molecules through Donor–Acceptor Interlocking by C–C Bonds**
by Tae Hui Kwon 1,Soon Ok Jeon 2,Masaki Numata 3,Hasup Lee 2,Yeon Sook Chung 2,Jong Soo Kim 2,Soo-Ghang Ihn 2,Myungsun Sim 2,Sunghan Kim 2 and Byeong Moon Kim 1


この中から``Scheme1``にある以下のテキストを取り上げることにします．

```text:scheme1
Synthetic routes of BMK-T138 and BMK-T139: (a) iodobenzene, K3PO4, CuI, N,N’-diacetylcyclohexane-1,2-diamine, dioxane, 80 °C; 86%; (b) triisopropyl borate, lithium 2,2,6,6-tetramethylpiperidide, tetrahydrofuran (THF), −60 °C, then 1 N HCl, r.t.; (c) Pd(OAc)2, tri-o-tolyphosphine, H2O, THF, 80 °C, 36% yield from 2 steps; (d) Carbazole, tBuOK, N,N-dimethylformamide (DMF), 160 °C, 49%; (e) Carbazole, tBuOK, DMF, 160 °C, 85%.
```


#Documentオブジェクト

まずは``Document``モジュールをimportします．

``Document``は目的とするテキストや文章をインスタンス化し，そこから**固有表現抽出（Named Entity Recognition; NER）**ができるようになります．上記の``scheme1``の文章から文章に含まれる化学物質名を得る**化学物質記述（chemical entity mention; CEM）**の操作は次のようにします．


```python
from chemdataextractor import Document

text = 'Scheme 1. Synthetic routes of BMK-T138 and BMK-T139: (a) iodobenzene, K3PO4, CuI, N,N’-diacetylcyclohexane-1,2-diamine, dioxane, 80 °C; 86%; (b) triisopropyl borate, lithium 2,2,6,6-tetramethylpiperidide, tetrahydrofuran (THF), −60 °C, then 1 N HCl, r.t.; (c) Pd(OAc)2, tri-o-tolyphosphine, H2O, THF, 80 °C, 36% yield from 2 steps; (d) Carbazole, tBuOK, N,N-dimethylformamide (DMF), 160 °C, 49%; (e) Carbazole, tBuOK, DMF, 160 °C, 85%.'
doc = Document(text)
doc.cems
```

すると，以下のような出力が得られます．

```py:出力
[Span('DMF', 377, 380),
 Span('THF', 296, 299),
 Span('CuI', 77, 80),
 Span('Pd(OAc)2', 260, 268),
 Span('tri-o-tolyphosphine', 270, 289),
 Span('DMF', 418, 421),
 Span('K3PO4', 70, 75),
 Span('iodobenzene', 57, 68),
 Span('Carbazole', 336, 345),
 Span('Carbazole', 400, 409),
 Span('H2O', 291, 294),
 Span('lithium 2,2,6,6-tetramethylpiperidide', 166, 203),
 Span('THF', 222, 225),
 Span('N HCl', 243, 248),
 Span('dioxane', 120, 127),
 Span('triisopropyl borate', 145, 164),
 Span('N,N’-diacetylcyclohexane-1,2-diamine', 82, 118),
 Span('tetrahydrofuran', 205, 220),
 Span('tBuOK', 347, 352),
 Span('tBuOK', 411, 416),
 Span('N,N-dimethylformamide', 354, 375)]
```
上記の出力のうち「N HCl」がおかしいですね．scheme1の本文をみると，「1 N HCl」（１規定の塩酸）を誤って認識してしまっていることがわかりますが，それ以外は概ね正確に抜き出しています．このように認識率は100%ではありませんが，低分子化合物であれば高い認識率で化学物質記述を得ることができそうです．

またTHFやDMFの略称は``abbreviation_definitions``から得ることができます．

```py
doc.abbreviation_definitions
```

```py:出力
[(['THF'], ['tetrahydrofuran'], 'CM'),
 (['DMF'], ['N,N-dimethylformamide'], 'CM')]
```

重複表示をなくし，化合物の固有情報（化合物名や略称）を一つにまとめるには，以下のrecordsから得ることができます．

```py
doc.records.serialize()
```

```py:出力
[{'names': ['iodobenzene']},
 {'names': ['K3PO4']},
 {'names': ['CuI']},
 {'names': ['N,N’-diacetylcyclohexane-1,2-diamine']},
 {'names': ['dioxane']},
 {'names': ['triisopropyl borate']},
 {'names': ['lithium 2,2,6,6-tetramethylpiperidide']},
 {'names': ['N HCl']},
 {'names': ['Pd(OAc)2']},
 {'names': ['tri-o-tolyphosphine']},
 {'names': ['H2O']},
 {'names': ['Carbazole']},
 {'names': ['tBuOK']},
 {'names': ['tetrahydrofuran', 'THF']},
 {'names': ['N,N-dimethylformamide', 'DMF']}]
```

``names``という属性で化合物名がまとめられていることがわかりますね．THFやDMFは略称と化合物名が``names``で一緒に表記されていることがわかります．

そのほかには，どのような属性があるのでしょうか．key-valueの関係から見てみると，

```py
doc.records[0].keys()
```
```py:出力
['names',
 'labels',
 'roles',
 'nmr_spectra',
 'ir_spectra',
 'uvvis_spectra',
 'melting_points',
 'glass_transitions',
 'quantum_yields',
 'fluorescence_lifetimes',
 'electrochemical_potentials']
```

このように属性には``names``のほかにもNMRやIRなどのスペクトル値や融点，ガラス転移点といった基本特性が含みうるようになっていることがわかります．keys()に対応するvalues()をみると，names以外は空欄ですが属性がネスト構造で格納できるようになっていることがわかりますね．

```py
doc.records[0].values()
```
```py:出力
[['iodobenzene'], [], [], [], [], [], [], [], [], [], []]
```

では，実際にそれらの属性に値が格納されるのかを別のテキストで見てみることにしましょう．
[Supplementary information](https://www.mdpi.com/2079-4991/9/12/1735/s1)のpdfにある2-(4,6-Diphenyl-1,3,5-triazin-2-yl)-9-phenyl-9H-3,9'-bicarbazole (BMK-T138) の合成手順とNMRによる同定が述べられているMethodをtext2へ入力し，そのNERをみてみることにします．

```python
text2 = "2-(4,6-Diphenyl-1,3,5-triazin-2-yl)-9-phenyl-9H-3,9'-bicarbazole (BMK-T138) : To a solution of carbazole 5a (2.98 g, 6.05 mmol) in DMF (18 mL) was added carbazole (6.07 g, 36.3 mmol) and tertBuOK (3.4 g, 30.3 mmol) at r.t.. The reaction mixture was stirred at 170 oC for 18 hours. The resulting mixture was cooled and filtered using MeOH. The filter cake was dried in reduced pressure at 60 oC for 4 hours. The resulting residue was purified by using flash column chromatography on silica-gel (Hex:CH2Cl2=1:1) to afford BMK-T138 (13.65 g, 49%). 1H-NMR (400 MHz, CDCl3, ppm) δ 8.06 (d, J = 7.6 Hz, 1H), 8.05 (d, J = 8.0 Hz, 2H), 7.94 (d, J = 7.2 Hz, 4H), 7.68-7.76 (m, 4H), 7.52-7.57 (m, 4H), 7.42 (t, J = 7.2 Hz, 2H), 7.28-7.38 (m, 6H), 7.19-7.26 (m, 5H). 13C-NMR (100 MHz, CDCl3, ppm) δ 172.7, 171.1, 142.6, 142.5, 140.3, 137.2, 135.6, 133.8, 132.1, 130.1, 128.8, 128.7, 128.2, 128.1, 127.6, 127.2, 126.3, 125.8, 123.2, 122.6, 122.5, 121.2, 120.8, 120.2, 119.2, 113.9, 110.4, 109.7. MALDI-MS Calcd: 639.24, Found: 639.26"
doc2 = Document(text2)
doc2.records.serialize()
```
```py:出力
[{'names': ["2-(4,6-Diphenyl-1,3,5-triazin-2-yl)-9-phenyl-9H-3,9'-bicarbazole"]},
 {'names': ['carbazole']},
 {'names': ['DMF']},
 {'names': ['MeOH']},
 {'names': ['silica']},
 {'names': ['Hex']},
 {'names': ['CH2Cl2']},
 {'names': ['1H']},
 {'names': ['2H']},
 {'nmr_spectra': [{'frequency': '400',
    'frequency_units': 'MHz',
    'nucleus': '1H',
    'peaks': [{'coupling': '7.6',
      'coupling_units': 'Hz',
      'multiplicity': 'd',
      'number': '1H',
      'shift': '8.06'},
     {'coupling': '8.0',
      'coupling_units': 'Hz',
      'multiplicity': 'd',
      'number': '2H',
      'shift': '8.05'},
     {'coupling': '7.2',
      'coupling_units': 'Hz',
      'multiplicity': 'd',
      'number': '4H',
      'shift': '7.94'},
     {'multiplicity': 'm', 'number': '4H', 'shift': '7.68-7.76'},
     {'multiplicity': 'm', 'number': '4H', 'shift': '7.52-7.57'},
     {'coupling': '7.2',
      'coupling_units': 'Hz',
      'multiplicity': 't',
      'number': '2H',
      'shift': '7.42'},
     {'multiplicity': 'm', 'number': '6H', 'shift': '7.28-7.38'},
     {'multiplicity': 'm', 'number': '5H', 'shift': '7.19-7.26'}],
    'solvent': 'CDCl3'}]},
 {'names': ['13C']},
 {'nmr_spectra': [{'frequency': '100',
    'frequency_units': 'MHz',
    'nucleus': '13C',
    'peaks': [{'shift': '172.7'},
     {'shift': '171.1'},
     {'shift': '142.6'},
     {'shift': '142.5'},
     {'shift': '140.3'},
     {'shift': '137.2'},
     {'shift': '135.6'},
     {'shift': '133.8'},
     {'shift': '132.1'},
     {'shift': '130.1'},
     {'shift': '128.8'},
     {'shift': '128.7'},
     {'shift': '128.2'},
     {'shift': '128.1'},
     {'shift': '127.6'},
     {'shift': '127.2'},
     {'shift': '126.3'},
     {'shift': '125.8'},
     {'shift': '123.2'},
     {'shift': '122.6'},
     {'shift': '122.5'},
     {'shift': '121.2'},
     {'shift': '120.8'},
     {'shift': '120.2'},
     {'shift': '119.2'},
     {'shift': '113.9'},
     {'shift': '110.4'},
     {'shift': '109.7'}],
    'solvent': 'CDCl3'}]},
 {'names': ['CDCl3']}]
```
NMRについてはケミカルシフトが1Hおよび13Cともキレイに格納されていますね．

参考までに，1H-NMRはrecords[9]に格納されており，さらにネスト構造でfrequency，frequency_units，nucleus，peaksの属性が階層になっています．最も低磁場側のケミカルシフトを得るには，peaks[0]のリストから以下のように取り出すことができます．


```py
doc2.records[9].nmr_spectra[0].peaks[0].values()
```
```py:出力
['8.06', None, 'd', '7.6', 'Hz', '1H', None]
```

合成結果のデータベースも，これらの情報から組み上げることができそうな予感がしますね．

以上，シンプルテキストによる固有表現抽出でした．

## 参照
* chemdataextractorのマニュアル：http://chemdataextractor.org/docs/intro