2019년 1월 25일 금요일

이공계 연구를 위한 H2O Driverless의 활용 - 분자 에너지 값의 예측


이번에는 화학이나 제조 공정 연구 등에 H2O DriverlessAI를 활용하는 가능성에 대해서 보도록 하겠습니다.  신물질 개발이나 기계적 특성 연구 등에는 다양한 성분 또는 온도, airflow 등의 다양한 조건들의 결합과 그에 따른 결과값 예측이 필요합니다.  그러나 비용과 시간의 문제 때문에 그 엄청난 수의 조합에 대해 모두 다 일일이 테스트를 해볼 수는 없지요.  공장이나 연구실의 각종 계측기를 통해 수집한 data가 어느 정도 축적되어 있다면, machine learning을 통해 기존 data를 분석하여 가장 좋은 결과값을 낼 성분 및 조건 등에 대한 조합을 미리 예측할 수 있습니다.  그를 통해 실제 테스트 회수를 크게 줄일 수 있으므로 비용 절감은 물론이고 더 빠른 개발도 가능합니다.  이때 그 machine learning이 빨리 이루어질 수록, 그리고 그 accuracy가 정확할 수록 그 효과는 커질 것입니다.

Kaggle에 올라온 public dataset 중에는 분자 및 그 내부의 원자 구조, 그리고 그에 따른 분자의 에너지값을 담은 json file들이 있습니다.

https://www.kaggle.com/burakhmmtgl/predict-molecular-properties/home

이 dataset은 다음과 같은 zip 형태로 download 받을 수 있는데, 이걸 unzip 해보면 10개의 json file들이 들어 있습니다.

[u0017649@sys-96775 files]$ unzip ./predict-molecular-properties.zip
Archive:  ./predict-molecular-properties.zip
  inflating: pubChem_p_00000001_00025000.json
  inflating: pubChem_p_00025001_00050000.json
  inflating: pubChem_p_00050001_00075000.json
  inflating: pubChem_p_00075001_00100000.json
  inflating: pubChem_p_00100001_00125000.json
  inflating: pubChem_p_00125001_00150000.json
  inflating: pubChem_p_00150001_00175000.json
  inflating: pubChem_p_00175001_00200000.json
  inflating: pubChem_p_00200001_00225000.json
  inflating: pubChem_p_00225001_00250000.json

각 json file들의 크기와 row 수는 아래와 같습니다.

[u0017649@sys-96775 files]$ ls -l *.json
-rw-rw-r--. 1 u0017649 u0017649 111430473 Aug 14  2017 pubChem_p_00000001_00025000.json
-rw-rw-r--. 1 u0017649 u0017649 115752953 Aug 14  2017 pubChem_p_00025001_00050000.json
-rw-rw-r--. 1 u0017649 u0017649 119400902 Aug 14  2017 pubChem_p_00050001_00075000.json
-rw-rw-r--. 1 u0017649 u0017649 116769374 Aug 14  2017 pubChem_p_00075001_00100000.json
-rw-rw-r--. 1 u0017649 u0017649 116383795 Aug 14  2017 pubChem_p_00100001_00125000.json
-rw-rw-r--. 1 u0017649 u0017649 122537630 Aug 14  2017 pubChem_p_00125001_00150000.json
-rw-rw-r--. 1 u0017649 u0017649  96286512 Aug 14  2017 pubChem_p_00150001_00175000.json
-rw-rw-r--. 1 u0017649 u0017649 126743707 Aug 14  2017 pubChem_p_00175001_00200000.json
-rw-rw-r--. 1 u0017649 u0017649 129597062 Aug 14  2017 pubChem_p_00200001_00225000.json
-rw-rw-r--. 1 u0017649 u0017649 147174708 Aug 14  2017 pubChem_p_00225001_00250000.json

[u0017649@sys-96775 files]$ wc -l *.json
   4924343 pubChem_p_00000001_00025000.json
   5096119 pubChem_p_00025001_00050000.json
   5258731 pubChem_p_00050001_00075000.json
   5159241 pubChem_p_00075001_00100000.json
   5120705 pubChem_p_00100001_00125000.json
   5401395 pubChem_p_00125001_00150000.json
   4234059 pubChem_p_00150001_00175000.json
   5570529 pubChem_p_00175001_00200000.json
   5698247 pubChem_p_00200001_00225000.json
   6485207 pubChem_p_00225001_00250000.json
  52948576 total

각각의 json 파일에는 En(에너지 값), atoms(원자 종류와 구조), id(일련번호), shapeM(shape multipole)의 4개 값이 들어있습니다.

{
'En': 37.801,
'atoms': [
{'type': 'O', 'xyz': [0.3387, 0.9262, 0.46]},
{'type': 'O', 'xyz': [3.4786, -1.7069, -0.3119]},
{'type': 'O', 'xyz': [1.8428, -1.4073, 1.2523]},
{'type': 'O', 'xyz': [0.4166, 2.5213, -1.2091]},
{'type': 'N', 'xyz': [-2.2359, -0.7251, 0.027]},
{'type': 'C', 'xyz': [-0.7783, -1.1579, 0.0914]},
{'type': 'C', 'xyz': [0.1368, -0.0961, -0.5161]},
...
{'type': 'H', 'xyz': [1.5832, 2.901, 1.6404]}
],
'id': 1,
'shapeM': [259.66, 4.28, 3.04, 1.21, 1.75, 2.55, 0.16, -3.13, -0.22, -2.18, -0.56, 0.21, 0.17, 0.09]
}

이 json 파일 하나하나에는 다음과 같이 18205개의 atoms 항목이 들어있습니다.

[u0017649@sys-96775 files]$ grep atoms pubChem_p_00000001_00025000.json | wc -l
18205

json 파일 하나의 row 수가 4924343이니까 4924343/18205 = 270  즉, 하나의 atoms 항목 안에 270줄의 data가 들어있는 셈이지요.

[u0017649@sys-96775 files]$ grep type pubChem_p_00000001_00025000.json | wc -l
565479

또 json 파일 하나에는 type이라는 단어가 565479 줄이 나옵니다.

[u0017649@sys-96775 files]$ echo "565479/18205" | bc -l
31.06174127986816808569

즉 하나의 atoms 당 평균 31개의 type가 존재한다는 것인데, 그나마 모든 atoms 항목에 균등한 개수의 type이 들어있는 것도 아니라는 뜻입니다.  하긴 각 분자마다 들어있는 원자의 종류와 개수가 각기 다를 수 밖에 없지요.

특히 atoms와 shapeM라는 항목들은 그 하나하나가 비정형 array로 되어 있습니다.  즉 분자마다 들어있는 원자 개수도 다르고 그에 따라 shape multipole 값도 다릅니다.  이런 비정형 array로 되어 있는 string 값을 분석하여 일정한 pattern을 모델링한다는 것은 매우 어려운 일이 될 것입니다.

이를 수치적으로 분석하려고 해도, atoms 항목 내의 저 많은 값들을 어떻게 분리하고 어떤 이름의 column으로 재정비해야 할지 골치가 아플 수 밖에 없습니다.  원래 그런 고민스러운 작업을 feature engineering이라고 하지요.  이 feature engineering을 어떻게 하느냐에 따라 machine learning으로 만들어낸 model의 성능과 accuracy가 크게 좌우됩니다.  이런 숙제는 data scientist들에게 돌아가는데, 숙련된 data scientist에게도 이는 크게 골치 아픈 일이며 또 과중한 업무 부담으로 이어지게 됩니다.

하지만 이 모든 것을 간단하게 해결해주는 것이 바로 H2O DriverlessAI입니다 !  H2O DriverlessAI의 가장 큰 혜택 중 하나가 자동화된 feature engineering 아니겠습니까 ?

하지만 전에 H2O DriverlessAI는 comma(,)나 pipe(|) 등으로 분리된 CSV 파일이나 Excel(xls, xlsx) 파일만 다룰 수 있다고 하지 않았던 가요 ?  저런 json 파일도 가공없이 통째로 분석할 수 있나요 ?   불행히도 그렇게는 안됩니다.

하지만 json 파일을 csv 파일로 가공하는 것은 매우 쉽습니다.  저는 개발자가 아니며 python program이라고는 Hello World 조차 제대로 할 줄 모르는 젬병이지만, 구글링해보면 구할 수 있는 아래의 샘플 코드 하나로 금방 이걸 전환할 수 있었습니다.

https://gist.github.com/pbindustries/803464d20f48a0a23d5934e3d11dadd6

위의 github에 올려진 sample을 이용하여 아래와 같이 j2c.py라는 이름의 매우 간단한 python code를 짰습니다.  짠 것이 아니라 그대로 copy & paste 했습니다.

[u0017649@sys-96775 files]$ vi j2c.py

import csv, json, sys
#check if you pass the input file and output file
if sys.argv[1] is not None and sys.argv[2] is not None:
    fileInput = sys.argv[1]
    fileOutput = sys.argv[2]
    inputFile = open(fileInput) #open json file
    outputFile = open(fileOutput, 'w') #load csv file
    data = json.load(inputFile) #load json content
    inputFile.close() #close the input file
    output = csv.writer(outputFile) #create a csv.write
    output.writerow(data[0].keys())  # header row
    for row in data:
        output.writerow(row.values()) #values row

이제 이를 이용하여 json 파일들을 csv 파일 형태로 변환하겠습니다.

[u0017649@sys-96775 files]$ for i in `ls *.json`
> do
> python j2c.py ./$i ./${i}.csv
> done

거의 날로 먹기지요 ?  아래와 같이 순식간에 csv 파일들이 새로 생성되었습니다.

[u0017649@sys-96775 files]$ ls *.csv
pubChem_p_00000001_00025000.json.csv  pubChem_p_00125001_00150000.json.csv
pubChem_p_00025001_00050000.json.csv  pubChem_p_00150001_00175000.json.csv
pubChem_p_00050001_00075000.json.csv  pubChem_p_00175001_00200000.json.csv
pubChem_p_00075001_00100000.json.csv  pubChem_p_00200001_00225000.json.csv
pubChem_p_00100001_00125000.json.csv  pubChem_p_00225001_00250000.json.csv

csv 파일 속의 Row 수는 header까지 포함하여 18206, 즉 atoms 개수대로 만들어졌습니다.

[u0017649@sys-96775 files]$ wc -l pubChem_p_00000001_00025000.json.csv
18206 pubChem_p_00000001_00025000.json.csv

각 csv의 형태는 아래와 같습니다.  En, id, shapeM, atoms의 4개 column으로 되어있는데, shapeM과 atoms는 여전히 무지막지한 형태의 비정형 string으로 되어 있습니다.  id column은 분석에는 사실상 무의미한 column이지요.  (보시기 편하도록 제가 shapeM에는 빨간색, atoms에는 파란색으로 글자색을 바꿨습니다.)

[u0017649@sys-96775 files]$ head -n 2 pubChem_p_00000001_00025000.json.csv
En,id,shapeM,atoms
37.801,1,"[259.66, 4.28, 3.04, 1.21, 1.75, 2.55, 0.16, -3.13, -0.22, -2.18, -0.56, 0.21, 0.17, 0.09]","[{u'xyz': [0.3387, 0.9262, 0.46], u'type': u'O'}, {u'xyz': [3.4786, -1.7069, -0.3119], u'type': u'O'}, {u'xyz': [1.8428, -1.4073, 1.2523], u'type': u'O'}, {u'xyz': [0.4166, 2.5213, -1.2091], u'type': u'O'}, {u'xyz': [-2.2359, -0.7251, 0.027], u'type': u'N'}, {u'xyz': [-0.7783, -1.1579, 0.0914], u'type': u'C'}, {u'xyz': [0.1368, -0.0961, -0.5161], u'type': u'C'}, {u'xyz': [-3.1119, -1.7972, 0.659], u'type': u'C'}, {u'xyz': [-2.4103, 0.5837, 0.784], u'type': u'C'}, {u'xyz': [-2.6433, -0.5289, -1.426], u'type': u'C'}, {u'xyz': [1.4879, -0.6438, -0.9795], u'type': u'C'}, {u'xyz': [2.3478, -1.3163, 0.1002], u'type': u'C'}, {u'xyz': [0.4627, 2.1935, -0.0312], u'type': u'C'}, {u'xyz': [0.6678, 3.1549, 1.1001], u'type': u'C'}, {u'xyz': [-0.7073, -2.1051, -0.4563], u'type': u'H'}, {u'xyz': [-0.5669, -1.3392, 1.1503], u'type': u'H'}, {u'xyz': [-0.3089, 0.3239, -1.4193], u'type': u'H'}, {u'xyz': [-2.9705, -2.7295, 0.1044], u'type': u'H'}, {u'xyz': [-2.8083, -1.921, 1.7028], u'type': u'H'}, {u'xyz': [-4.1563, -1.4762, 0.6031], u'type': u'H'}, {u'xyz': [-2.0398, 1.417, 0.1863], u'type': u'H'}, {u'xyz': [-3.4837, 0.7378, 0.9384], u'type': u'H'}, {u'xyz': [-1.9129, 0.5071, 1.7551], u'type': u'H'}, {u'xyz': [-2.245, 0.4089, -1.819], u'type': u'H'}, {u'xyz': [-2.3, -1.3879, -2.01], u'type': u'H'}, {u'xyz': [-3.7365, -0.4723, -1.463], u'type': u'H'}, {u'xyz': [1.3299, -1.3744, -1.7823], u'type': u'H'}, {u'xyz': [2.09, 0.1756, -1.3923], u'type': u'H'}, {u'xyz': [-0.1953, 3.128, 1.7699], u'type': u'H'}, {u'xyz': [0.7681, 4.1684, 0.7012], u'type': u'H'}, {u'xyz': [1.5832, 2.901, 1.6404], u'type': u'H'}]"

이 10개의 파일들로부터 마지막 5줄씩을 미리 잘라내어 총 50줄 (column까지 합하면 51줄)의 pubChem_test1.xlsx라는 test용 dataset을 만들어두겠습니다.

그리고나서 5줄씩 줄어든 이 10개의 파일들을 upload 하기 편하도록 하나의 pubChem1.zip 파일로 zip으로 압축하겠습니다.

[u0017649@sys-96775 files]$ zip pubChem1.zip *.csv
  adding: pubChem_p_00000001_00025000.json.csv (deflated 78%)
  adding: pubChem_p_00025001_00050000.json.csv (deflated 78%)
  adding: pubChem_p_00050001_00075000.json.csv (deflated 78%)
  adding: pubChem_p_00075001_00100000.json.csv (deflated 78%)
  adding: pubChem_p_00100001_00125000.json.csv (deflated 77%)
  adding: pubChem_p_00125001_00150000.json.csv (deflated 78%)
  adding: pubChem_p_00150001_00175000.json.csv (deflated 77%)
  adding: pubChem_p_00175001_00200000.json.csv (deflated 78%)
  adding: pubChem_p_00200001_00225000.json.csv (deflated 78%)
  adding: pubChem_p_00225001_00250000.json.csv (deflated 78%)


이제 H2O DAI의 web interface에 접속합니다.  Dataset 메뉴에서 이 pubChem1.zip을 H2O DAI 서버로 upload하고 'Details' 항목을 보겠습니다.



보시는 바와 같이 En과 id는 각각 real과 integer로 인식되는데, shapeM과 atoms는 무지막지한 길이와 형태의 string으로 인식됩니다.



하지만 그냥 H2O DAI가 알아서 제대로 해줄 것이라고 믿고 그냥 그대로 prediction (training)으로 들어가겠습니다.  우리가 예측하려는 분자의 energy 값인 En을 Target column으로 정하고 Accuracy와 Time, Interpretability는 각각 10, 7, 7 정도로 세팅해서 돌리겠습니다.



참고로 이렇게 10-7-7로 맞출 경우의 algorithm과 iteration 회수, 그리고 model 및 feature 개수 등은 아래와 같이 설정됩니다.  위 사진의 왼쪽 상세 부분인데 글자가 너무 작아 잘 안 보이실 것 같아서 확대해서 캡춰했습니다.



이제 'Launch experiment'를 클릭하여 training을 시작합니다.  제가 가난하여 GPU가 없는 관계로, 이 training은 모두 2-core짜리 POWER8 가상 머신에서 수행했습니다.  (SMT8 때문에 H2O는 이를 2 * 8 = 16-core 짜리 장비라고 인식합니다.)  그래서 꽤 오랜 시간이 걸렸습니다.



Training 중간 과정을 보면 중앙 하단에 'Variable Importance'라는 항목이 보입니다.  이는 dataset 내부의 여러 column 중 어느 column이 En 값 예측에 가장 중요한지 중요도 순으로 보여주는 것인데, 이 값들은 training이 진행됨에 따라 변하기도 하고 새로 나타나기도 합니다.  보시면 우리가 우겨넣은 column은 분명히 En, id, shapeM과 atoms 4개 밖에 없었는데, 이 메뉴에 보여지는 column 이름들은 atoms_0, atoms_18 등 새로운 column 이름들이 많이 나온 것을 보실 수 있습니다.  즉, H2O DAI가 내부에서 자동으로 feature engineering을 수행한 것이지요.



이제 experiment, 즉 training이 끝났습니다.  Train된 model을 이용하여 prediction을 해보도록 하겠습니다.

'Score on Another Dataset'이라는 항목을 클릭한 뒤, 아까 따로 잘라놓았던 50줄짜리 pubChem_test1.xlsx를 선택합니다.  그러면 이 excel 표의 shapeM과 atoms column을 분석하고 아까 만들어진 model에 대입한 뒤, En 값이 어떨지 예측을 하여 그 결과를 csv 파일로 download 시켜줍니다.

실제 En 값과 H2O DAI가 예측한 En 값을 비교하여 그래프로 만들면 아래와 같습니다.  나름 꽤 그럴싸한 예측을 했다는 것을 보실 수 있습니다. 




이렇게 H2O DAI는 data scientist의 업무 부담을 크게 줄여주고 더 빠른 feature engineering과 modeling을 통해 화학, 생명, 제조 등의 연구실에서도 유용하게 사용하실 수 있습니다.


댓글 3개:

  1. 마지막에 실제값과 예측값을 항상 띄워주시는데 저부분이 h2o.ai에 탑재되어 있어도 좋을 것 같네요 ㅎㅎ

    답글삭제
    답글
    1. H2O에 탑재된 ROC Curve에서 Accuracy로 확인하셔도 됩니다.

      삭제
  2. 또는 H20 왼쪽화면하단에 나오는 RMSE(잔차 제곱 평균의 제곱근)로 확인하셔도 될 것 같습니다.

    답글삭제