딥러닝(RNN)을 이용한 화재 조기감지 프로젝트

2021. 7. 15. 10:25화재 조기감지 프로젝트

혹시 화재로 인해 직접 피해를 입었거나 그것을 목격한 경험이 있나요? 지금부터 소개할 프로젝트는 화재가 발생했을 때 피해를 일으키기 전 단계인 초기 분해 단계에서 화재를 감지하는 스마트센서를 개발하는 프로젝트입니다. 초기 분해 단계는 시설물에 피해가 없는 단계로, 초미립자 분해 생성물이 발생하지만 연기가 아직 눈에 보이지 않는 단계입니다.

화재의 확산 단계 - 주간기술동향, "데이터 중심의 스마트시티 재난 예방 및 조기 감지 기술 동향", 2019. 12. 18.

프로젝트에서 제가 맡은 역할은 여러 가스센서로 다양한 환경에서 측정한 화재 데이터를 가공해서, 가공한 데이터를 RNN으로 학습시켜 어느 환경에서도 화재 조기감지가 가능하도록 동작시키는 역할입니다. 이 역할을 수행하기 위해 다음과 같은 순서로 프로젝트를 진행했습니다.

  1. 데이터 확보
  2. 데이터 전처리
    • 데이터 분석 및 형태 정리 
    • 핵심 데이터 추출(데이터 분리)
    • 노이즈 제거
  3. RNN 돌려보기

 

실행 환경: PyCharm Community Edition 2017.3.3

 

1. 처음 주어진 것들 - 데이터 확보

화재 조기감지를 위해 온도와 습도를 측정하는 센서 2개, 초미립자 분해 생성물을 측정하는 가스센서 8개를 준비했습니다. 모듈을 2개로 나눠서 센서를 장착했으며 8가지의 가스 센서는 아래와 같습니다. D1~D8은 각각의 화합물에 대한 변수인데, 이후 데이터로 변환할 때 데이터의 속성 이름으로 쓰입니다.

  • Module01
    • D1: NiO(산화니켈, 특히 황화수소 가스 감지에 탁월한 성능을 지님)
    • D2: In2O3 + Au
    • D3: SnO2(산화주석)
    • D4: TGS823(알코올 센서, SnO2 사용)
  • Module02
    • D5: In2O3(산화인듐)
    • D6: WO3(산화 텅스텐(VI))
    • D7: Fe2O3(산화 철)
    • D8: TGS826(암모니아 가스 센서)

 

각각의 센서에 쓰인 화합물들은 모두 금속 산화물이고, 금속 산화물은 반도체의 특성을 가지고 있으며 금속과 산소의 이온결합으로 이루어져 있습니다. 산화물 반도체식 가스 센서는 금속 산화물 반도체의 특성을 이용하여 기체 상태의 물질을 검출하는 센서입니다.

50, 100, 200, 350도의 온도에서 각각 시료를 가열하여 위의 센서들로 이를 측정하는 실험을 진행했습니다. 실험 결과는 엑셀 파일과 txt파일로 저장이 되었는데, txt파일의 초기 상태는 아래와 같았습니다.

Left: Module01 / Right: Module02

이 실험은 10월(in 한국)에 진행했음에도 TEMP와 HUMI의 온도가 이상해서 처음에는 데이터에 오류가 있는지 의심이 되었습니다. 그러나 데이터상의 TEMP, HUMI는 보드판 상의 온, 습도였음을 깨닫고 데이터에 대한 의심을 거뒀습니다. 

 

2. 데이터 전처리(데이터 가공)

데이터 분석 및 형태 정리

- 초기 데이터 가공: 상황(다양한 온도 등)에 따라 센서를 측정한 데이터들(그래프 포함)은 엑셀파일에 저장되어 있었지만 딥러닝에 돌릴 수 있는 데이터셋에 알맞게 가공해야 했습니다. 때문에 먼저 필요한 데이터 이외의 데이터는 삭제하고, 필요한 데이터는 정렬했습니다. 위 txt파일 사진을 보면 필요없는 부분들과 데이터가 뒤섞인 부분, 맞지 않는 속성 네임들이 보이기 때문에 먼저 이를 삭제하거나 수정해 줬습니다. 

 

- class 설정: 이후 화재가 나지 않았다/났다를 0/1로 구분한 이진 class 속성을 추가해주기 위해 실험의 결과를 정리한 자료를 보았습니다. 자료는 아래와 같습니다.

 

  a. 전기화재에 민감하게 반응하기 위해 voltage값을 측정한 데이터. 화재가 발생하면 값이 증가한다.

  b. 센서들로 측정한 가스 농도에 대한 센서 저항 값 데이터. 화재가 발생하면 값이 줄어든다. (NiO 제외)

  c. b 데이터를 그래프로 표현

센서 저항값 출력. 저항값과 가스 값은 대체적으로 반비례 관계임을 알 수 있음. 

-> 측정된 가스 값을 그래프로 표현해 보니 데이터의 범위가 너무 크고, D1은 혼자 상승곡선을 그리고 있어 비교가 어려웠습니다. 때문에 D1은 역수를 취하여 1/D1으로 비교하기로 하고 b 데이터를 정규화(normalization)했습니다.

 

  d. b 데이터의 normalization

-> 이 때의 normalization은 Min-Max Normalization(최소-최대 정규화)를 사용한 것입니다. 정규화에 대한 내용은 아래를 참고하세요!

 

  • Min-Max Normalization(최소-최대 정규화)
    • 최소-최대 정규화는 모든 feature(요소)를 0~1 사이의 값(최대:1, 최소:0)들로 변환하는 방법
    • (X-MIN) / (MAX-MIN)
    • 그러나 첫 번째 정규화 방법은 이상치(outlier)에 너무 많은 영향을 받는다는 단점이 존재함. 이는 예를 들어 100개의 값이 주어졌을 때, 해당 값 중 99개는 0~40 사이에 있고, 나머지 하나만 100이라면 99개의 값이 모두 0~0.4 사이의 값으로 변환된다는 예제로 이해할 수 있음.
  • Z-Score Normalization(Z-점수 정규화)
    • Z-점수 정규화는 위의 방법의 단점인 이상치(outlier) 문제를 보완한 기법
    • (X-평균) / 표준편차
    • if, feature의 값 == 평균 -> 0으로 정규화
    • feature < 평균 -> 음수 / feature > 평균 -> 양수가 됨.
    • 이 때 계산되는 음수/양수의 크기 = feature의 표준편자에 의해 결정됨.

 

  e. d 데이터를 그래프로 표현

left: 1/D1~D8 데이터 / right: 데이터 중 D6, D7 데이터가 너무 튀어서 제거함.

 

위와 같은 데이터들을 비교 · 분석한 결과 50~350도 중 350도가 가장 명확한 변화를 보여서 일단 Module01의 350도 데이터만 코드로 돌려보기로 했습니다.(위의 데이터 사진들은 모두 350도 데이터입니다.) 이제 드디어 위의 그래프를 기준으로 class를 지정합니다. 350도 실험에서의 가열 시작점(보라색 수직선)과 이후 그래프가 급격히 변화하는 시점(파란색 수직선)에 각각 직선을 그어 놓고 파란색 수직선을 기준으로 class의 0/1을 지정했습니다. 이 class 구분선은 이번 프로젝트의 핵심 쟁점으로써, 이 파란색 수직선을 얼마나 앞당길 수 있는지에 따라 프로젝트의 성과가 결정될 것으로 보입니다.

 

데이터 분리

- 의미 있는 데이터 분리: RNN으로 돌릴 데이터는 위 그래프의 데이터인 센서 저항값이 아니라 데이터 a.번째에 있던 voltage값을 사용했기 때문에 voltage 데이터를 csv파일로 변환하여 전처리를 시작했습니다. csv파일 변환은 voltage 데이터가 본래 xlsx 파일이었기 때문에 다른 이름으로 저장을 통해 확장자를 변경해 주는 식으로 했습니다.

 

프로젝트에서 파란색 수직선 즉, class의 구분점을 앞당기는 것이 관건인 지금, 저에게 중요한 데이터는 저 그래프 중 보라색 수직선과 파란색 수직선의 사잇값이 됩니다. 나머지 데이터는 반경 몇십 sec 말고는 의미가 없다고 판단하여 분홍색 구간만 남기고 그 이외의 데이터는 파일에서 직접 잘랐습니다. 

드디어 코드로써 데이터를 만질 차례가 왔습니다. pandas의 read_csv()로 csv파일을 읽어올 수 있습니다.

#Step1. 변화구간 중심으로 데이터 분리
df = pd.read_csv('../fire_dataset/module01-split_ver/350_sp.csv') # 350으로 테스트

 

- 핵심 데이터 속성 추출: 불러온 voltage 데이터는 dataframe 형태로써, 출력하면 아래와 같은 형태를 띱니다. 

voltage data의 dataframe

아래의 그래프는 위 dataframe을 그래프로 표현한 것인데, 왼쪽 그래프를 보면 데이터 속성의 개수가 많은 것을 느낄 수 있습니다. 이렇게 되면 RNN으로 학습할 때 혼란을 가중시킬 수 있다는 우려가 생길 수 있기 때문에 class 기준선인 연두색 선을 기준으로 가장 먼저 변화를 보인 D2만 추출해서 RNN을 돌려 보기로 했습니다. 특정 행을 지정할 때는 df['D2']와 같이 작성하면 됩니다. 

# 그래프 출력 코드
import numpy as np
import matplotlib.pyplot as plt

x_len = np.arange(len(df))
plt.axvline(164, c='olivedrab') # label='fire start point'
plt.axvline(360, c='yellowgreen') # label='class turning point'
#plt.plot(x_len, df, c='royalblue') # label='original data' -> 전체 datafrmae 출력
plt.plot(x_len, df['D2'], c='violet', label='D2') # -> D2만 출력
plt.legend(loc='upper right')
plt.grid()
plt.xlabel('time(sec)')
plt.ylabel('voltage(value)')
plt.show()

left: voltage data 그래프 출력 / right: D2 추출

 

튀는값 다듬기 - 노이즈 제거

지금까지 출력해 본 그래프에서는 다소 거친 형태의 곡선을 확인할 수 있었습니다. 곡선이 거칠다는 것은 노이즈가 많다는 의미와 동일하므로, 이처럼 노이즈가 너무 많으면 딥러닝을 돌려 학습할 때 클래스 구분에 어려움을 가져올 수 있기 떄문에 ewma를 사용해서 곡선을 다듬기로 했습니다.

 

노이즈를 제거하는 방법은 매우 다양합니다. 그 중에서 ewma는 이동평균(Moving Aveage)을 이용한 노이즈 제거 방법인데, 그렇다면 이동평균이란 무엇일까요?

 

- 이동평균의 등장배경: 이동평균은 주식 시장에서 시작된 말입니다. 요즘 너도 나도 주식하는 게 유행인데.., 유행의 선두를 달리고 있는 주식 시장에는 이동평균선이라는 지표가 있다고 합니다. 예를 들어 10일 이동평균선이라고 하면 과거 10일 동안의 주가를 평균낸 값을 계속 이어서 표시하는 방법이라는 의미입니다. 주가는 예측할 수 없이 움직인다는 것을 '랜덤워크가설'이라고 하는데, 이를 극복해 보고자 예측할 수 없는 움직임이라도 평균을 내보면 어떠한 방향성을 찾을 수 있지 않을까?라는 생각을 하게 되었고, 바로 여기서 이동평균선이 등장하게 되었다고 합니다.

 

- 이동평균의 유형: 이동평균에는 3가지 유형이 있습니다.

  • 단순이동평균(Simple Moving Average, SMA)
    • Window를 정해서 그 갯수만큼 평균을 내는 방식. 
    • ex) window=6 -> 6일치의 평균을 일별데이터가 업데이트할 때마다 계속 구하는 것임.
    • 가장 구하기 쉽고 직관적임. 
    • 그러나 데이터의 갑작스런 증폭을 반영하는 시간이 좀 느리고, 특정시점에 따라 변동성이 굉장히 달라지며 변동성 Clustering 효과를 잡아줄 수 없다는 단점이 존재함.  
      sma_12 = df.rolling(window=12).mean()
  • 가중이동평균(Weighted Moving Average, WMA)
    • 현재에 가까운 데이터가 과거의 데이터보다 더 중요하다는 전제로 동작.
    • 금융 데이터에 적용해 보면, n일 가중이동평균에서 최신 날짜의 가중치: n / 두 번째 최신 날짜의 가중치: n-1...
    • 이렇게 가중치가 1이 될 때까지 줄어듦.
      wikipedia, "이동평균"
       
    • 그러나, 여전히 이용한 기간(여기서의 기간이란,,, 주식 관련 데이터이기 때문에 한 사람이 주식시장을 이용한 기간을 말하는 것) 값의 데이터만을 평균으로 반영하기 때문에 실제로는 잘 사용하지 않는다고 함.
    • 함수가 없어서 따로 구현이 필요함.
    • def weighted_mean(w_arr):
          def inner(x):
              return (w_arr*x).mean()
          return inner
      weights = np.arange(1,13)
      wma_12 = df['D2'].rolling(12).apply(lambda voltage:np.dot(voltage, weights)/weights.sum(), raw=True)
  • 지수이동평균(Exponential Moving Average, EMA) 또는 지수가중이동평균(Exponentially Weighted Moving Average, EWMA)
    • 주로 위의 그래프처럼 시간순으로 나열되어 있는 시퀀스(sequence) 데이터에 활용되며 현재를 기준으로 오래된 값을 가중치를 낮게 부여하고, 최근 값은 가중치를 높게 부여해서 평균값을 도출하는 방식
    • 가중치는 Exponential Function(지수함수)에 근거하여 도출됨.
    • 코드 적용할 때 ewma()함수도 있는 모양이지만 잘 적용이 되지 않아서 ewm()에서 span(=window)값을 적당히 준 후 .mean()을 붙여 사용함. 
    • ewma_12 = df.ewm(span=12).mean() # span: window 크기 or 기간 지정 /350: span=12

 

- 3종 비교: 단순이동평균(SMA), 가중이동평균(WMA), 지수가중이동평균(EWMA)을 각각 window=12로 프로젝트 데이터에 적용해 보았습니다. 확대한 그래프를 보면 모두 오리지널 데이터인 df를 따라 그래프가 형성되었지만 조금씩 차이를 보이는 것을 확인할 수 있습니다. 이중 그래프의 변화를 실시간으로 잘 반영하여 곧잘 그래프가 변화하는 곡선은 EWMA로 확인되었습니다. 

결론적으로, 데이터 전처리에서 데이터의 변화에 빠르게 반응하고자 EWMA를 사용한 것입니다. 

left: window=12일때 테스트 / right: 확대버전

 

- 번외: window 크기에 따른 3종 비교 (window=12, window=52)

window 크기에 따라 그래프가 어떻게 변화하는지 궁금하셨죠? 저도 궁금해서 돌려보니 window 크기가 커질수록 그래프의 곡선이 완만해지네요! (위쪽 곡선들: window=12, 아래쪽 곡선들: window=52)

 

- D2 추출로 급격히 줄어든 데이터양 늘리기

이렇게 추출한 데이터만 남으면 학습할 데이터가 너무 적기 때문에 i번째 데이터를 100개씩 분리(=window를 100으로 잡음)하여 중복되는 데이터셋을 만들었습니다. 즉, 0번째 low 데이터: 0~99개, 1번째 low 데이터: 1~100개, 2번째 low 데이터: 2~101개, ... 이런 식으로 데이터셋을 생성했다는 의미입니다.

 

  • 데이터 생성 중에 transpose()를 이용해서 데이터의 행열을 전환하여 .values.flatten()으로 100개씩 분리된 데이터들이 한 줄 한 줄 다음 low로 추가될 수 있도록 했습니다.  
  • 여기까지 수정한 데이터는 아래와 같은 형태를 띠게 되는데, 이 데이터를 보고 소수점 아래 자릿수들이 많아 애매한 값들이 발생할 수 있다는 우려가 생겼습니다. 때문에 round()로 반올림하여 아래 오른쪽과 같이 깔끔한 값으로 변경해 주었습니다.

left: round() 전(0:5행만 print) / right: round() 후

  • 마지막으로 class를 추가해주면 데이터 전처리는 끝이 나게 됩니다. 'CLASS'라는 이름의 열을 마지막 열에 추가하고, iloc함수로 0~320번째 행에 해당하는 100번째 열의 값을 0으로 지정하고, 321~마지막 행까지는 1로 지정하여 class를 구분해 주었습니다. 첫 번째 iloc함수 코드는 처음 class열을 추가할 때 해당 열 전체를 0으로 초기화 해주었기 때문에 지워도 무방할 것 같습니다. 이렇게 전처리가 완료된 데이터는 to_csv()함수로 csv파일에 저장했습니다. 
#Step3. Data 분리 후 특정 속성만 뽑아 확인
i=0
result = pd.DataFrame()
result = ewma[['D2']][0:0+100] # result에 columns값을 넣어주지 않으면 에러남. 때문에 넣은 코드임.
result = result.transpose()

for i in range(i, len(ewma)-1):
    sp_df23 = ewma[['D2']][i:i+100] #100개씩 분리
    if i == len(ewma)-99:
        break

# Step3-1. dataframe transpose(행열 전환) - 목표: 100행n열 만들기
    sp_df23 = sp_df23.transpose()
    result.loc[i] = sp_df23.values.flatten() # 한 줄 한 줄 밑으로 추가한 후 1차원으로 변경
result.drop('D2', inplace=True) #result에 넣은 값 하나 제거하기. 중복되기 때문

#Step3-2. 애매한 값을 쳐내기 위한 정수 변환 - round()로 반올림
result = result.round(0).astype(int)

#Step3-3. class column 추가
result['CLASS'] = 0
result.iloc[:321, 100] = 0
result.iloc[321:, 100] = 1
result.to_csv('.../fire_dataset/module01-100splits/350_100sp.csv', index=None)

csv로 저장된 데이터

 

 

가스센서에 대해 좀 더 알고싶다면... -> https://blog.daum.net/highpower/15966226 or 밑 논문 참고

 

[Reference]

센서 관련:

이정석, "Zn와 Sn의 비율과 α-Fe2O3의 몰 농도에 따른 α-Fe2O3/ZnO n-n 접합 나노구조체 가스센서 연구", 2019.

normalization:

http://hleecaster.com/ml-normalization-concept/

ewma:

https://ai-hyu.com/preprocessing-ewma/

https://kanggogo1234.tistory.com/40

https://www.cmegroup.com/ko/education/learn-about-trading/courses/technical-analysis/understanding-moving-averages.html#

https://wendys.tistory.com/178

https://ko.wikipedia.org/wiki/%EC%9D%B4%EB%8F%99%ED%8F%89%EA%B7%A0