수행기록퀘스트6

[GA] Quest 6 STM32Cube.AI와 B-L475E-IOT01A 보드를 이용하여 본인만의 AI 프로젝트 만들기 - 사각지대 지각을 위한 음향 장면 인식 프로젝트
2022. 9. 26 (월) 23:10 최종수정 2022. 10. 23 (일) 20:51 칩헤드 조회 4708 좋아요 2 스크랩 0 댓글 22

> 가속도 센서를 응용하시거나 (1) 다양한 센서의 데이터 로깅을 통해 STM32Cube.AI에서
> 지원가능한 상용 머신러닝툴을 사용하여 (2) Neural Network Model을 만들고
> (3) STM32Cube.AI로 변환한 C code Model를 적용한 (4) 어플리케이션을 자유롭게 만들어 보시면 됩니다.
>
> 1. 개발하신 어플리케이션이 B-L475E-IOT01A 보드에서 구동하는 동영상을 업로드 해주세요.
> 2. 상용 머신러닝 툴에서 사용한 Source Code를 업로드 해주세요.
> 3. B-L475E-IOT01A에 적용된 Application Source Code와 해당 Application에 대한 소개 및 설명을 포함하는 PDF 문서를 업로드하세요.
> (PDF 문서에는 2번의 머신러닝 툴에서의 Model에 대한 설명도 포함 합니다.
> 예를 들어 Model의 구성 설명이나 Accuracy 등과 같은 내용을 설명해 주시면 됩니다.)

 

 

Quest6 미션 과제의 수행 결과를 다음과 같이 포스팅합니다. IOT 개발보드를 이용하여 데이타의 학습 및 추론을 하면서
서버 및 데스크탑 PC와 달리 CPU/RAM 자원이 극히 제한된 임베디드 환경에서 알아야 할 주요 기술 지식들을
하나하나 정확이 이해를 하면서 피부로 경험할수 있는 더 없이 좋은 시간이었습니다. ^^

 

                                        [B-L475E-IOT01A개발보드의 개발환경 셋업 화면]

 

 

                                                            - 다 음 - 

 

            (STM32Cube.AI와 B-L475E-IOT01A 보드를 이용한 ASC-AI 프로젝트)

Q6: 사각지대 지각을 위한 음향 장면 인식 프로젝트

 

 

참고) 아래의 목차중에서 해당 링크를 클릭하면 해당 부분으로 이동하도록 북마킹 되어 있습니다. 

 

목차

프로젝트 소개... 2

프로젝트 목표... 2

소프트웨어 준비... 4

IoT보드 준비... 6

오디오 데이터 생성... 7

오디오 데이터 그룹 만들기... 7

오디오 데이터 수집하기... 9

Data를 (.wav) 생성하기... 9

Data로 LogMel Spectrogram 생성... 12

LogMel Spectrogram으로 전처리 작업... 12

피쳐 표준화하기... 15

데이터 인코딩하기... 15

데이터셋을 훈련, 검증, 테스트 셋으로 분리... 16

(선택사항) 피쳐 저장하기... 16

AI 모델 학습... 17

AI 모델 그래프 만들기... 17

AI 모델 빌드하기... 18

AI 모델 훈련하기... 18

Accuracy 평가하기... 21

Confusion Matrix 평가하기... 21

생성한 AI 모델 저장... 22

(선택사항) 양자화로 TFLite 모델 변환... 23

STM32.XCube AI 툴... 24

AI 모델을 C코드로 변환... 24

C 소스코드의 빌드 및 플래싱... 25

데모... 27

 

 

 

 



 

 

프로젝트 소개

이 섹션은 프로젝트의 목표 및 개발을 위한 환경 준비에 대한 내용을 설명합니다.

 

프로젝트 목표

저는 음향 이벤트 인식 기술은 오디오 신호에서 발생하는 이벤트 종류를 찾는 문제로, 최근 많은 연구가 이루어 지고 있습니다. 음향 장면 인식 기술은 시각적으로 확인할 수 없는 사각지대에 발생한 예상치 못한 상황을 소리만으로도 인식할 수 있습니다. 이 때문에 음향 장면 인식 기술을 활용하면 (1) 청력이 저하된 사람을 위한 공간 지각 서비스, (2) 안전 상황 감시, (3) 실내 상황 모니터링을 위한 AI 스피커 등 실생활에 매우 다양한 응용 기술의 개발이 가능합니다. 이러한 음향 장면 인식 기술의 발전을 위하여, 매년 IEEE 기관에서 DCASE Challenge (https://dcase.community/) 이라는 세계적인 인공지능 기반 음향 장면 인식 기술 대회가 개최할 정도로 미래 사회에서 중요한 기술들 중의 하나입니다.

 

[DCASE Challenge 국제 대회의 ASC 입력 및 출력 방법 예시]

 

이번 자율 프로젝트에서는 ST IoT 개발보드와 STM32 Cube AI 툴을 이용하여 실내외 환경 및 차량내 환경에서 발생할 수 있는 다양한 소리들을 탐지하고 인공지능을 이용하여 더 똑똑하게 소리를 인식할 수 있는 음향 장면인식 프로젝트에 도전해보려고 합니다. 일반적으로 음성 인식은 음향 인식에 언어처리 부분을 추가합니다. 예를 들어, 음성입력 -> 특징 추출 -> 특징 벡터 -> 패턴 분류 -> 후보단위 -> 언어처리 -> 인식된 문장 이런 순서가 됩니다.

[소리 데이터의 학습 및 테스트 흐름도]

저는 딥러닝 기술을 이용하여 IoT 장치의 마이크가 캡처한 주변 소음을 분류할 수 있습니다. 이번 프로젝트에서는 행사 측에서 제공하는 IoT 개발보드에 내장되어 있는 마이크 장치를 이용하여 사운드를 입력 받고, 학습 후 AI모델을 만들 것입니다. 그리고나서 만든 AI모델을 C소스코드로 변환하고, 컴파일하여 펌웨어 이미지를 만들고, 이 펌웨어 이미지를 개발보드에 플래싱한 후에 음성 데이터가 Indoor음성인지, Outdoor음성인지, In-Vehicle 음성인지 분류하는 딥러닝 소프트웨어를 데모하려고 합니다.

 

제일 먼저 저는 사운드 입력을 3 가지 범주로 분류하기 위해 CNN이라는 딥러닝 모델을 생성해야 합니다. 다음 그림은 입력되는 음향 데이터들을 3개의 클래스로 분류후에 분류된 음향 데이터들을 학습하여 유사 음향 데이터가 입력으로 들어올 때 어떤 음향 데이터인지 인공지능을 통해 분류하는 작업을 위한 전체 개념도입니다.

[음성 데이터를 Indoor, Outdoor, In-Vehicle으로 분류하기 위한 개념도]

  • Indoor (집안)
  • Outdoor (집밖)
  • In-Vehicle (차량내)

 

 

 

 

소프트웨어 준비

저는 수집한 음성 데이터들을 이용하여 모델 생성과 모델 학습을 달성하기 위하여 Keras소프트웨어를 사용할 것입니다. 그리고, 이 작업을 위하여 ST Microelectronics에 의해 제공되는 STM32용 FP-AI-SENSING1이라는 기본 패키지를 활용할 것입니다. IOT 개발보드를 통하여 입력되는 음성 데이터들을 사전 처리 및 피쳐 추출을 해야 합니다. 이 작업을 위하여 저는 전 세계적으로 잘 알려져 있는 오픈소스 Librosa라이브러리를 이용합니다. 

[오디오 파일의 전처리 및 CNN 학습 흐름도]

 

파이썬 버전에 따른 패키지들의 의존성이 상당히 민감하기 때문에 사용중인 운영체제의 안정성이 파괴되지 않도록 주의해야 합니다. 그래서 파이썬 소프트웨어를 다룰 때는, 가급적 Conda와 같은 파이썬 패키지의 가상화 환경 프로그램을 활용하는 것을 추천하고 싶습니다. 파이썬 패키지 설치는 pip install {package-name} 수행하도록 합니다. 만약, 이미 설치된 파이썬 패키지가 있다면, pip install --upgrade {package-name} 를 수행하여 패키지 버전을 업그레이드합니다.

import os
import numpy as np
from tqdm import tqdm
import matplotlib.pyplot as plt
 
from sklearn.model_selection import train_test_split
from sklearn import preprocessing
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
 
import keras
from keras import layers
from keras import models
from keras import optimizers
 
# import tensorflow as tf
import tensorflow.compat.v1 as tf
#To make tf 2.0 compatible with tf1.0 code, we disable the tf2.0 functionalities
tf.disable_eager_execution()
 
import librosa.display

import librosa.util

 

이 프로젝트를 수행하기 위하여 사용된 Keras, TensorFlow, Librosa 패키지들의 버전정보는 다음과 같습니다. 이때 여기서 Keras는 딥러닝 프론트 엔드가 되고, TensorFlow은 딥러닝 백엔드의 역할을 수행합니다.

print("Python:", python.__version__)
print("Keras:", keras.__version__)
print("TensorFlow:",tf.__version__)
print("librosa:",librosa.__version__)
 
Keras (expected version: 2.2.4):  2.2.4
TensorFlow (expected version: 1.14.0):  1.14.0
librosa (expected version: 0.9.2):  0.9.2

 

 
 
 

IoT보드 준비

본 프로젝트를 수행하기 위하여 IOT보드에 마이크 장치가 포함되어 있어야 한다. 마이크 장치는 사용자 레벨의 애플리케이션 수준에서 녹음이 가능할 수 있도록 디바이스 드라이버와 저 수준의 시스템콜 라이브러리가 지원되어야 합니다. 

다음은 B-L475E-IOT01A 보드의 하드웨어 스펙을 보여줍니다. 저는 해당 IOT보드가 마이크 기능을 (MP34D501) 지원하는지 데이터 시트 문서를 통하여 사전 체크해야 합니다. ST사의 "STM32F413RHT" Micom의 "DFSDM"기능이 Sigma-Delta입력 필터 기능을 수행을 하기 때문에 AMC1306칩의 출력인 Uncoded Bit stream형태의 신호를 MCU에서 받아서 내부 필터 기능을 적용하여 데이터화 합니다.

 

 

[B-L475E-IOT01A 개발보드의 하드웨어 정보]

 

 

오디오 데이터 생성

 

오디오 데이터 그룹 만들기

이 섹션은 오디오 데이터의 정보를 설명합니다. 실제 애플리케이션을 빌드하려면 완전한 데이터 세트를 사용하는 것이 좋습니다. 예를 들어, TUT Acoustic Scenes 2016 데이터셋을  (https://zenodo.org/record/45739#.YzjqqXblKUk) 사용할 수 있습니다. 각각의 데이터 세트는 다른 녹음 위치를 가진 다양한 음향 장면의 녹음으로 구성됩니다. 데이터 세트는 Dataset이라는 폴더 안에 위치시킵니다. 데이터세트에는 각 행에 오디오 파일에 대한 경로와 공백으로 구분된 레이블이 있는 메타데이터 파일을 다음과 같이 포함합니다.

  1. bus.wav bus
  2. park.wav park
  3. home.wav home

원래 클래스(버스, 카페/레스토랑, 자동차, 등등)는 단순성을 위해 보다 일반적인 클래스(실내, 실외, 차량)로 집계됩니다. 런타임에 데이터가 로딩이 될 때, 샘플비율은16kHZ으로 재샘플링이 되고, 채널은 mono으로 변환됩니다.

주의) 오디오 신호 샘플링 속도 및 채널 번호 설정은 STM32 오디오 캡처 설정과 일치해야 합니다.  

 

dataset_dir = './Dataset'
meta_path = path = os.path.join(dataset_dir, 'TrainSet.txt')
fileset = np.loadtxt(meta_path, dtype=str)
 
# 3 classes : 0 indoor, 1 outdoor, 2 in-vehicle
class_names = ['indoor', 'outdoor', 'vehicle']
labels = {
    'bus' : 2,
    'cafeRestaurant' : 0,
    'car' : 2,
    'cityCenter' : 1,
    'home' : 0,
    'office' : 0,
    'park' : 1,
    'residentialArea' : 1,
    'shoppingCenter' : 0,
    'subway' : 2,
    'train' :  2,
    'tramway' :  2,
}
 
x = []
y = []
 
for file in tqdm(fileset):
    file_path, file_label = file
    file_path = os.path.join(dataset_dir, file_path)
    # @note: resampling can take some time!
    signal, _ = librosa.load(file_path, sr=16000, mono=True, duration=30, dtype=np.float32)
    label = labels[file_label]
    x.append(signal)
    y.append(label)
100%|??????????| 3/3 [00:00<00:00, 106.86it/s]

 

 

 

 
plt.figure(figsize=(12, 2))
plt.title(class_names[y[0]])
librosa.display.waveshow(x[0], sr=16000)
 
plt.figure()
plt.plot(x[0])
plt.xlabel('Sample Index')
plt.ylabel('Amplitude')
plt.title('Waveform of Test Audio (Play)')
plt.show()
 

 

 

 

[주파스 레벨 측정 결과]

 

 

오디오 데이터 수집하기

일반적으로 사람의 귀는 소리를 비선형적으로 받아들입니다. 보통 청각의 감지 능력은 500Hz 이하의 소리에서는 1Hz의 변화도 구별을 할 수 있습니다. 그러나 2,000Hz 이상에서는 5Hz정도의 변화를 감지할 수 있다고 합니다. 즉 낮은 주파수에서는 작은 변화에도 민감하지만, 높은 주파수로 갈수록 민감도가 작아집니다. 이러한 청각의 특성을 반영하여 주파수 성분을 얻는 모델을 Mel Spectrogram이라고 합니다.

원래의 오디오 데이터를 신경망 모델에 제공하는 대신에 저는 LogMel Spectrogram이라는 '피쳐 (Feature)' 세트를 생성하기 위해 먼저 데이터를 더 작은 서브프레임으로 슬라이싱을 수행해야 합니다. 이러한 기능은 모델 학습, 검증 및 테스트에 사용되기 위한 것입니다.

 

 

Data를 (.wav) 생성하기

IoT개발보드의 마이크기능을 이용하여 .wav 포맷의 오디오 파일을 녹음을 합니다.

     

 

 

수집된 오디오 파일은 IOT 개발보드의 사용자 공간 (8MB)에 저장이 된다. IOT 개발보드의 USB 저장장치로부터 녹음한 오디오 파일을 가져오는 방법은 다음과 같습니다.

  1. 1단계, IOT 개발보드 뒷면의 JP4 점퍼선을 중간으로 (5~6)으로 다시 꼽습니다.
  2. 2단계, OT 개발보드의 STLink 포트가 아닌 USB OTG 포트에 USB 케이블을 연결합니다.
  3. 3단계, RESET 버턴을 누른 상태에서 USER 버턴을 눌려서 IOT 개발보드를 부팅합니다.
  4. 4단계, 다음과 같이 ST IOT 보드의 저장 장치에 보관된 IoT01****.wav 파일을 Host PC로 가져옵니다.

 

[ST IOT 개발보드에 저장한 wave  파일의 오디오 속성 정보]

 

다음 그림은 30초짜리, 256kbps 비트 전송률로 생성된 3타입의 .wav 파일을 보여주고 있습니다. 아래의 예처럼 각 wav 파일들을 30초 짜리 파일들을 녹화하도록 합니다.

[Host PC의 “Dataset”폴더에  옮겨놓은 wave 파일 정보 내역]

 

Data로 LogMel Spectrogram 생성

각 프레임에는 16,896개의 샘플 (=32 x 512 + 512)이 포함됩니다. 그리고 다음 예제 코드에서 보여주는 것처럼, n_fft=1024 및 hop_length=512인 32열의 Spectrogram을 생성합니다.

 

x_framed = []
y_framed = []
 
for i in range(len(x)):
    frames = librosa.util.frame(x[i], frame_length=16896, hop_length=512)
    x_framed.append(np.transpose(frames))
    y_framed.append(np.full(frames.shape[1], y[i]))
    
# merge sliced frames and label
x_framed = np.asarray(x_framed)
y_framed = np.asarray(y_framed)
x_framed = x_framed.reshape(x_framed.shape[0]*x_framed.shape[1], x_framed.shape[2])
y_framed = y_framed.reshape(y_framed.shape[0]*y_framed.shape[1], )
 
print("x_framed shape: ", x_framed.shape) # Each frame of 16,896 samples can be used to create spectrogram 
print("y_framed shape: ", y_framed.shape) # Corresponding label for each frame
x_framed shape:  (2715, 16896)
y_framed shape:  (2715,)

 

 

 

LogMel Spectrogram으로 전처리 작업

LogMel Spectrogram 특징점을 추출해야 합니다. 이 추출방법을 사용하여 특징 세트를 생성합니다. 이때 파이썬의 tqdm 함수를 이용하면 진척되는 현황을 모니터링할 때 편리합니다.   

중요: 전처리 매개변수는 STM32 구현에 정의된 parameter와 일치해야 합니다. 

 

x_features = []
y_features = y_framed
 
for frame in tqdm(x_framed):
    # Create a mel-scaled spectrogram
    S_mel = librosa.feature.melspectrogram(y=frame, sr=16000, n_mels=30, n_fft=1024, hop_length=512, center=False)
    # Scale according to reference power
    S_mel = S_mel / S_mel.max()
    # Convert to dB
    S_log_mel = librosa.power_to_db(S_mel, top_db=80.0)
    x_features.append(S_log_mel)
 
# Convert into numpy array
x_features = np.asarray(x_features)
100%|??????????| 2715/2715 [00:12<00:00, 218.58it/s]

 

 

 

특징점을 추출한 피쳐세트은 2,715개의 기능이 있습니다. 각 기능은 30 x 32크기의 Spectrogram으로 표시됩니다.

print(x_features.shape)
(2715, 30, 32)

 

 

다음 파이썬 코드는 피쳐 클래스를 위해 생성되는 Spectrogram을 그래프로 그리는 예제입니다. Plot 파이썬 모듈을 이용하여 그래프로 그리는 작업을 보여줍니다.

# Plot the first spectrogram generated for each feature class
 
plt.figure(figsize=(10, 8))
plt.subplot(311)
indoor_index = np.argmax(y_framed == 0)
librosa.display.specshow(x_features[indoor_index], sr=16000, y_axis='mel', fmax=8000,
                         x_axis='time', cmap='viridis', vmin=-80.0)
plt.colorbar(format='%+2.0f dB')
plt.title('LogMel spectrogram for ' + class_names[y_features[indoor_index]])
 
plt.subplot(312)
outdoor_index = np.argmax(y_framed == 1)
librosa.display.specshow(x_features[outdoor_index], sr=16000, y_axis='mel', fmax=8000,
                         x_axis='time', cmap='viridis', vmin=-80.0)
plt.colorbar(format='%+2.0f dB')
plt.title('LogMel spectrogram for ' + class_names[y_features[outdoor_index]])
 
plt.subplot(313)
vehicle_index = np.argmax(y_framed == 2)
librosa.display.specshow(x_features[vehicle_index], sr=16000, y_axis='mel', fmax=8000,
                         x_axis='time', cmap='viridis', vmin=-80.0)
plt.colorbar(format='%+2.0f dB')
plt.title('LogMel spectrogram for ' + class_names[y_features[vehicle_index]])
 
plt.tight_layout()
plt.show()

 

 
 

 

 

 

[3개로 분리한 클래스별 LogMel Spectrogram  정보 그래프]

 

피쳐 표준화하기

이제 평균을 제거하고 단위 분산에 맞게 조정하는 작업을 수행합니다. 피쳐 표준화를 위한 샘플 x의 표준 점수는 다음과 같이 계산합니다.

z = (x - u) / s

x: 샘플 값

u: 훈련 샘플의 평균

s: 훈련 샘플의 표준 편차

z: 표준화 점수

 

다음과 같이 스케일링을 위하여 피쳐들을 재구성을 합니다. 이때 피쳐 스케일러로 피쳐 변형을 수행합니다.

# https://scikit-learn.org/stable/auto_examples/preprocessing/plot_scaling_importance.html
# Flatten features for scaling
x_features_r = np.reshape(x_features, (len(x_features), 30 * 32))
 
# Create a feature scaler
scaler = preprocessing.StandardScaler().fit(x_features_r)
 
# Apply the feature scaler 
x_features_s = scaler.transform(x_features_r)

 

 
 

데이터 인코딩하기

각 피쳐에는 각각 일치하는 레이블이 있습니다. 그러나 Keras에는 범용적인 One-Hot 인코딩 대상의 레이블 데이터가 필요합니다. 따라서, 레이블을 원하는 인코딩으로 변환해 보겠습니다.

# Convert labels to categorical one-hot encoding
y_features_cat = keras.utils.to_categorical(y_features, num_classes=len(class_names))
 
print("y_features_cat shape: " ,y_features_cat.shape)
y_features_cat shape:  (2715, 3)

 

 
 

데이터셋을 훈련, 검증, 테스트 셋으로 분리

저는 훈련할 때 이전에 본 적이 없는 데이터에 대한 모델의 정확도를 확인합니다. 그래서, 저는 데이터 세트를 훈련 및 검증 세트로 분할하여 각 훈련 Epoch이 끝날 때 손실 및 기타 모델 메트릭을 평가하기를 원합니다. 이 작업의 목표는 훈련 및 검증 데이터만 사용하여 모델을 개발하고 조정하는 것입니다. 테스트 세트는 알려지지 않은 새 데이터인 것처럼 최종 평가에만 사용됩니다.

 

# how to traing models with skleran
# https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html#sklearn.model_selection.train_test_split
x_train, x_test, y_train, y_test = train_test_split(x_features_s, y_features_cat, test_size=0.25, random_state=1)
x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.25, random_state=1)
print('Training samples:', x_train.shape)
print('Validation samples:', x_val.shape)
print('Test samples:', x_test.shape)
Training samples: (1527, 960)
Validation samples: (509, 960)
Test samples: (679, 960)

 

 

 

(선택사항) 피쳐 저장하기

마지막으로 X-CUBE-AI가 이해할 수 있는 형식으로 피쳐들을 .csv 파일 포맷으로 저장합니다. 각 텐서에 대해 값이 평평한 벡터에 있습니다.

out_dir = './Output/'
np.savetxt(out_dir + 'x_train.csv', x_train.reshape(len(x_train), 30 * 32), delimiter=",")
np.savetxt(out_dir + 'y_train.csv', y_train, delimiter=",")
np.savetxt(out_dir + 'x_val.csv', x_val.reshape(len(x_val), 30 * 32), delimiter=",")
np.savetxt(out_dir + 'y_val.csv', y_val, delimiter=",")
np.savetxt(out_dir + 'x_test.csv', x_test.reshape(len(x_test), 30 * 32), delimiter=",")
np.savetxt(out_dir + 'y_test.csv', y_test, delimiter=",")

 

 
 

AI 모델 학습

이제 AI 모들을 생성하기 위하여 학습하고 평가하는 작업을 수행할 차례입니다. 
 

AI 모델 그래프 만들기

이제 CNN 분류기 모델을 구축할 차례입니다. model.add 파이썬 함수를 이용하여 각 레이어별로 수행하고자 하는 내용들을 선언해주도록 합니다.

model = models.Sequential()
model.add(layers.Conv2D(16, (3, 3), activation='relu', input_shape=(30, 32, 1), data_format='channels_last'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(16, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(9, activation='relu'))
model.add(layers.Dense(3, activation='softmax'))
 
# print model summary
model.summary()
WARNING: Logging before flag parsing goes to stderr.
The name tf.get_default_graph is deprecated. Please use tf.compat.v1.get_default_graph instead.
Please use tf.compat.v1.placeholder instead.
The name tf.random_uniform is deprecated. Please use tf.random.uniform instead.
The name tf.nn.max_pool is deprecated. Please use tf.nn.max_pool2d instead.
 
Layer (type)                 Output Shape              Param #   
===============================================
conv2d_1 (Conv2D)            (None, 28, 30, 16)        160       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 14, 15, 16)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 12, 13, 16)        2320      
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 6, 6, 16)          0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 576)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 9)                 5193      
_________________________________________________________________
dense_2 (Dense)              (None, 3)                 30        
==============================================
Total params: 7,703
Trainable params: 7,703
Non-trainable params: 0
 

 

 
 

AI 모델 빌드하기

Loss Function을 계산할 때 전체 Train-Set을 사용하는 것을 Batch Gradient Descent라고 합니다. 그러나 이렇게 계산하면 한번 step을 내딛을 때마다 전체 데이터에 대해 Loss Function을 계산해야 합니다. 이렇게 되면 너무 많은 계산양을 필요로 합니다. 이를 방지하기 위해 Stochastic Gradient Descent (SGD)라는 방법을 사용합니다. 따라서, Loss Function을 계산할 때, 전체 데이터(Batch) 대신 일부 데이터의 모음(Mini-Batch)를 사용하여 Loss Function을 계산합니다. Batch Gradient Descent보다 다소 부정확할 수는 있지만, 계산 속도가 훨씬 빠르기 때문에 같은 시간에 더 많은 step을 갈 수 있습니다. 그 다음 AI 모델을 최종적으로 빌드하는 작업을 수행하도록  합니다.

sgd = optimizers.SGD(lr=0.01, momentum=0.9, nesterov=True)
model.compile(optimizer=sgd, loss='categorical_crossentropy',  metrics=['acc'])
The name tf.train.Optimizer is deprecated. Please use tf.compat.v1.train.Optimizer instead.
The name tf.log is deprecated. Please use tf.math.log instead.

 

 

 

AI 모델 훈련하기

이제 훈련 데이터를 모델에 입력할 차례입니다. 이 작업에는 많은 시간이 소요되기 때문에 고성능의 CPU 또는 GPU가 권장됩니다.

 

# Reshape features to include channel
x_train_r = x_train.reshape(x_train.shape[0], 30, 32, 1)
x_val_r = x_val.reshape(x_val.shape[0], 30, 32, 1)
x_test_r = x_test.reshape(x_test.shape[0], 30, 32, 1)

 

 

 

AI 모델을 학습하기 위하여 다음과 같이 model.fit 함수를 이용하여 지정된 epoch 횟수만큼 학습을 수행하도록 합니다.

# Train the model
history = model.fit(x_train_r, y_train, validation_data=(x_val_r, y_val),
                    batch_size=500, epochs=30, verbose=2)
 
Epoch 1/10
2022-10-01 15:36:08.448797: I tensorflow/core/platform/cpu_feature_guard.cc:142] Your CPU supports 
instructions that this TensorFlow binary was not compiled to use: AVX
 - 1s - loss: 1.1662 - acc: 0.1657 - val_loss: 1.0465 - val_acc: 0.6326
Epoch 2/10
 - 1s - loss: 1.0344 - acc: 0.6385 - val_loss: 0.9907 - val_acc: 0.6483
Epoch 3/10
 - 1s - loss: 0.9668 - acc: 0.6483 - val_loss: 0.8680 - val_acc: 0.6582
Epoch 4/10
 - 1s - loss: 0.8237 - acc: 0.6627 - val_loss: 0.6569 - val_acc: 0.6974
Epoch 5/10
 - 1s - loss: 0.6127 - acc: 0.7289 - val_loss: 0.4824 - val_acc: 0.8134
Epoch 6/10
 - 1s - loss: 0.4768 - acc: 0.8062 - val_loss: 0.3702 - val_acc: 0.9234
Epoch 7/10
 - 1s - loss: 0.3537 - acc: 0.9103 - val_loss: 0.2569 - val_acc: 0.9136
Epoch 8/10
 - 1s - loss: 0.2483 - acc: 0.9214 - val_loss: 0.1819 - val_acc: 0.9371
Epoch 9/10
 - 1s - loss: 0.1824 - acc: 0.9325 - val_loss: 0.1489 - val_acc: 0.9430
Epoch 10/10
 - 1s - loss: 0.1499 - acc: 0.9371 - val_loss: 0.1206 - val_acc: 0.9528
Evaluate model:
                         ……… Omission ………
 32/679 [>.............................] - ETA: 0s
352/679 [==============>...............] - ETA: 0s
672/679 [============================>.] - ETA: 0s
679/679 [==============================] - 0s 174us/step
[0.12382532915152576, 0.9587628865979382]
Test loss: 0.123825
Test accuracy: 95.88%
Accuracy = 95.88%

 

 
 
이제 학습된 AI 모델의 손실 현황 추이를 살펴보기 위하여 다음과 같이 시각화된 그래프로 출력합니다. 
train_loss = history.history['loss']
val_loss = history.history['val_loss']
 
plt.figure()
plt.clf()
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.plot(train_loss, color='r', label='training loss')
plt.plot(val_loss, color='g', label='validation loss')
plt.legend()
 

 

 

 

 

  

[학습 및 검증의 epoch 별 손실율 변화 그래프]

 

Accuracy 평가하기

다음으로, 모델이 테스트 데이터 세트에서 수행하는 방식을 비교합니다.

 

print('Evaluate model:')
results = model.evaluate(x_test_r, y_test)
print(results)
print('Test loss: {:f}'.format(results[0]))
print('Test accuracy: {:.2f}%'.format(results[1] * 100))
Evaluate model:
679/679 [==============================] - ETA:  - ETA:  - ETA:  - ETA:  - 0s 292us/step
[0.0033203214310954525, 1.0]
Test loss: 0.003320
Test accuracy: 100.00%

 

 

 

Overfitting (과적합) 은 머신 러닝 모델이 훈련 데이터보다 새로운 데이터에서 더 나쁜 성능을 보이는 경우입니다.

 

Confusion Matrix 평가하기

혼돈행렬 (Confusion Matrix)은 학습을 통한 Prediction 성능을 측정하기 위한 것입니다. Prediction value와 Actual value를 비교하기 위한 표입니다.

y_pred = model.predict(x_test_r)
 
y_pred_class_nb = np.argmax(y_pred, axis=1)
y_true_class_nb = np.argmax(y_test, axis=1)
 
accuracy = accuracy_score(y_true_class_nb, y_pred_class_nb)
np.set_printoptions(precision=2)
print("Accuracy = {:.2f}%".format(accuracy * 100))
 
cm = confusion_matrix(y_true_class_nb, y_pred_class_nb, labels=[0,1,2])
 
# (optional) normalize to get values in %
# cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
 
# Loop over data dimensions and create text annotations.
thresh = cm.max() / 2.
for i in range(cm.shape[0]):
    for j in range(cm.shape[1]):
        plt.text(j, i, format(cm[i, j], 'd'),
                ha="center", va="center",
                color="white" if cm[i, j] > thresh else "black")
 
plt.imshow(cm, cmap=plt.cm.Blues)
Accuracy = 100.00%
 

 

 

[Counfusion Matrix 의 그래프 출력 결과]

 

 

 

 

생성한 AI 모델 저장

딥러닝 환경에 훈련한 AI모델을 저장할 때, 일반적으로 파일 확장자는 .ckpt, .pb, .h5 등의 3개의 포맷이 범용적입니다. 각각의 파일 포맷의 주요 차이는 다음과 같습니다.

 

  1. ckpt: Pytorch에 주로 사용되는 포맷이다. 그래프 (=모델구조)만 있는 .ckpt-meta 파일, 가중치(weight)만 있는 .ckpt-data 파일로 구성된다.
  2. pb: 모델구조 (graph)와 가중치 (weight) 를 모두 가지고 있는 파일이다.
  3. h5: Keras에서 주로 사용하는 포맷이다. 그래프 (=모델구조)와 가중치(weight)를 모두 가지고 있는 파일이다.

 

이제 학습한 AI 모델을 .h5 파일 형식으로 저장하기 위하여 model.save( )명령을 수행합니다.  

 

# Save the model into an HDF5 file ‘model.h5’
model.save(out_dir + 'model.h5')

 

 
 

(선택사항) 양자화로 TFLite 모델 변환

TensorFlow Lite 변환기를 사용하여 추가적인 최적화를 수행할 수 있습니다. 일반적으로 TensorFlow Lite 변환기는 CPU 및 DRAM 메모리 용량이 제한적인 임베디드 장치들을 지원하기 위하여 정확도를 희생시키는 대신에 모델 크기가 줄이는 기능을 도모합니다. 본 작업을 위하여 아래의 사이트를 참고하여 양자화하는 방법에 대한 도움을 받았습니다.

 

x_train_r.shape
(1527, 30, 32, 1)

 

 

 

def representative_dataset_gen():
    for i in range(len(x_train_r)):
        yield [x_train_r[i].reshape((1, ) + x_train_r[i].shape)]
 
 
converter = tf.lite.TFLiteConverter.from_keras_model_file(out_dir + "model.h5" )
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset_gen
converter.target_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
 
tflite_model = converter.convert()
 
with open(out_dir + 'model.tflite','wb') as f:
    f.write(tflite_model)
    print("TFlite model saved")
W1024 11:46:57.122369 437772 deprecation.py:323] From c:\users\chiphead\appdata\local\programs\python
\python36\lib\site-packages\tensorflow\lite\python\util.py:238: convert_variables_to_constants 
(from tensorflow.python.framework.graph_util_impl) is deprecated and will be removed in a future version.
Instructions for updating:
Use `tf.compat.v1.graph_util.convert_variables_to_constants`
W1024 11:46:57.125878 437772 deprecation.py:323] From c:\users\chiphead\appdata\local\programs\python
\python36\lib\site-packages\tensorflow\python\framework\graph_util_impl.py:270: extract_sub_graph 
(from tensorflow.python.framework.graph_util_impl) is deprecated and will be removed in a future version.
Instructions for updating:
Use `tf.compat.v1.graph_util.extract_sub_graph`
TFlite model saved

 

 

 

STM32.XCube AI 툴

생성된 AI 모델을 IOT 개발보드에 실행가능한 C 소스코드로 변환합니다. 그리고 C소스코드를 빌드하는 작업을 하는 방법을 설명합니다. 

 

AI 모델을 C코드로 변환

XCube.MX 도구는 사전 훈련된 모델 .h5 파일을 로딩후, STM32 IOT 개발 장치를 위한 최적화된 C 언어 기반의 모델을 생성할 수 있습니다.  XCube.MX 도구를 실행하면 생성되는 asc.*, asc_data.* 파일들을 STM32CubeFunctionPack_SENSING1_V4.0.3의 소스 폴더에 복사 및 붙여넣기를 하도록 합니다. 
 

[STM32 Cube MX을 이용하여 .h5를 C코드로 변환하는 예시]
 

 

C 소스코드의 빌드 및 플래싱

STM32Cube IDE를 이용하여 생성한 ASC C소스코드를 빌드하여 펌웨어 이미지를 생성한다. 그리고나서 생성한 펌웨어 이미지를 IOT 개발보드에 플래싱을 수행하도록 한다. 

 

[MCU GCC 컴파일러: C/C++ 소스코드의 빌드 옵션]
 
 

[STM32 Cube IDE을 이용하여 C 소스 생셩 결과 예제]
 

 

 

데모

오디오 분류 페이지는 FP-AI-SENSING1과 함께 제공되는 ASC 알고리즘의 AI 신경망 분류 결과를 모니터링하는 데 사용할 수 있으며, 다음 인식된 오디오 장면 중 하나를 알릴 수 있습니다.

• Indoor
• Outdoor
• In-Vehicle
 

    [음향 소리 인식: 초기화]                     [음향 소리 인식: Home]                 [음향 소리 인식: Mic Plot Data]

 

 

STM32Cube MX의 ASC 모델의 모델구조(=Graph)와 가중치 (=Weight)를 분석합니다. AI 모델의 전체 레이어는 다음 그림에서 보여주는 바와 같이 컨볼류션 및 풀링을 통해 반복 학습을 수행하였고, 이렇게 학습한 내용들을 최종 레이어에서 Fully Connected Layer으로 구성하기 위하여 Dense 오퍼레이션을 수행하였습니다. 


    
[모델구조 세부 구조도]

 

 

다음 그림을 학습한 가중치 데이터들을 참고 있는 weight 배열의 정보들을 보여주고 있습니다. ST IOT 개발보드의 RAM 용량에 맞는 만큼 적절히 배열 사이즈를 조절해주어야 합니다. 그렇지 않으면 C 소스코드를 빌드 하는 과정에서 “region `RAM' overflowed by 4240 bytes” 와 같은 에러 메시지를 만나게 됩니다. 


 
[가중치 정보] 

 


* 데모: B-L475E-IOT01A 보드에서 구동하는 동영상

음향 장면 인식 기술은 시각적으로 확인할 수 없는 사각지대에 발생한 예상치 못한 상황을 소리만으로도 인식할 수 있습니다. 딥러닝 기술을 이용하여 IoT 장치의 마이크가 캡처한 주변 소음을 더 세밀하게 분류할 수 있습니다. 이번 프로젝트에서는 ST MicroElectronics사의 IoT 개발보드에 내장되어 있는 마이크 장치를 이용하여 음향을 입력받고, 학습후 AI모델을 만들었습니다. 그다음, 생성한 AI모델을 C 소스 코드로 변환하고, 컴파일하여 펌웨어 이미지를 만들고, 이 펌웨어 이미지를 개발보드에 플래싱합니다. 최종적으로 입력되는 음향을 분석함으로써, Indoor 음성인지, Outdoor 음성인지, In-Vehicle 음성인지 분류해내는 CNN 기반 딥러닝 소프트웨어를 데모합니다.

 

웹 링크

 

 

참고자료

  1. DCASE Challenge 2019, https://dcase.community/challenge2019/task-acoustic-scene-classification
  2. DCASE Challenge 2020, https://dcase.community/challenge2020/task-acoustic-scene-classification
  3. DCASE Challenge 2021, https://dcase.community/challenge2021/task-acoustic-scene-classification
  4. https://paperswithcode.com/task/acoustic-scene-classification
  5. Attention is all you need (NIPS 2017), https://arxiv.org/abs/1706.03762
  6. ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices (CVPR 2018), https://arxiv.org/abs/1707.01083
  7. A Comparative Study on Approaches to Acoustic Scene Classification using CNNs (CVPR 2022), https://arxiv.org/abs/2204.12177

 

 

용어

  1. ADC: Audio Analog-to-Digital Converters
  2. ASC: Acoustic Scene Classification
  3. CNN: Convolutional Neural Network
  4. CSV: Comma-Separated Value
  5. DMA: Direct Memory Access
  6. DCASE: Detection and Classification of Acoustic Scenes and Events
  7. DFSDM: Digital Filter for Sigma-Delta Modulator
  8. FFT: Fast Fourier transform
  9. HAL: Hardware Abstract Layer
  10. SGD: Stochastic Gradient Descent

 

이상. 

첨부파일
제출자료-Quest06-Chiphead-20221010.zip 다운로드
칩헤드
2022.10.13 15:45
To all, 본 행사 종료후, 홍익인간을 위해, ST 공식 커뮤니티에 커멘트 내용들을 추가 반영한 영문버젼 문서를 업로드하여 더 다양한 경험들을 듣고 소통해 볼 예정입니다.
칩헤드
2022.10.26 21:57
ST MicroElectronics의 공식 커뮤니티에 더 많은 ST 보드 사용자들과 공유를 위하여 포스팅 하였습니다. https://community.st.com/s/project/a8g3W000000EoLkQAK/asr-project-for-blind-spot-perception-with-stm32cubeai-on-bl475eiot01a-board
자작공작소
2022.10.12 13:46
Keras로 만든 ASC모델을 양자화 모델인 TFlite로 변환은 되지만 중요한 것은 TFlite모델은 X-CUBE-AI에서 C코드로 변환을 못합니다. 칩헤드님도 양자화 모델이 아닌 Keras 모델을 사용하셔서 FLOAT(AI_BUFFER_FORMAT_FLOAT)형으로 변환된 C코드를 사용하셨는데 이럴경우 생성된 C코드를 사용해서 빌드하면 언급하신 Heap메모리 초과 에러가 발생합니다. 칩헤드님 사용하신 가중치 값 버퍼 크기를 임의로 줄이면(16284->7840) 빌드도 정상적 되고 프로그램도 실행이 되지만 ASC NN의 기본 가중치 값 타입이 FLOAT형이어서 ASC NN 프로세싱이 정상적으로 되지 않습니다. 이전에 말씀드린 코드를 수정해야 정상적인 프로세싱이 가능합니다. 이 내용은 CubeIDE 프로젝트 빌드시 전처리기에 SENSING1_USE_PRINTF를 추가 하신 후 빌드하고 터미널 접속을 해보면 알 수 있습니다.
칩헤드
2022.10.12 16:41
커멘트 주신, SENSING1_USE_PRINTF 요거 LD_PRELOAD만큼이나 핵팁인데요? 멋지십니다. :)
칩헤드
2022.10.12 16:44
https://wiki.st.com/stm32mcu/wiki/AI:X-CUBE-AI_documentation#X-CUBE-AI_embedded_documentation_contents 에서 가이드하고 있는 STM32Cube.AI 에서 지원하는 DL/ML 프레임웍 (Keras, Tensorflow lite, ONNX, ONNX-ML op., DQNN 등 )중에서, 사용하시는 SDK 버젼에서는 Only Keras만 이식 및 C코드로의 변환하고 있다는 뜻인거세요? 흐미, 나중에 DQNN을 좀더 try해보려고 했는데말입니다.
자작공작소
2022.10.12 17:12
저는 Keras와 TFlite모델만 변환을 해 보았습니다. 저도 기본 예제로 들어있는 0 indoor, 1 outdoor, 2 in-vehicle 예제는 실행해 보았으나 인식률이 그렇게 좋지는 않았습니다. 칩헤드님게서 제작하신 모델을 IOT보드로 포팅 했을때 인식률이 어떻던가요? 위에 알려드린 코드내의 버그를 수정하고는 ASC 프로세싱이 제대로 수행되어 인식률이 아주 좋았습니다.
칩헤드
2022.10.12 17:14
제가 튜닝한 모델의 경우엔 Confusion Matrix에서는 indoor가 가장 안좋았구요. 그래도 3개 모두 accuracy가 높은 편이었는데, Cube.MX툴이 모델이 C코드로 변환한 소스로, 실제 IOT보드에서 추론(inference) 수행시, in-vehicle의 인식률이 구렸드랬습니다. 하여 Cube.MX 툴이 아직 모델구조와 가중치를 완전하게 generate을 못하고 있구나 했구요. 그러면서, 지급 받은 IoT개발보드의 Datasheet보면, NPU가 없는데, IOT보드에서 그냥 MCU-4 (ARM-M)만으로 inference하는거 자체가 적정한가? 고민해보면서, 혼자서 scholar.google.com 에서 관련 연구논문들을 킁킁되던 참이었습니다.
자작공작소
2022.10.12 17:23
기본 예제는 소스코드를 보면 가중치 데이터 타입(AI_BUFFER_FORMAT_U8)이 양자화된 TFlite모델을 사용해서 만든 것으로 보입니다. 그래서 그런지 인식률이 그렇게 좋지 못합니다. 저도 TFlite모델을 X-CUBE-AI 4.1.0에서 변환하려고 하였으나 오류가 나면서 변환을 하지 못했습니다. 그래서 그 뒤가 험난해 졌습니다.
칩헤드
2022.10.12 17:31
넵, 공작소님이랑 토킹을 하면서 들은 생각인데, CUBE.AI 버젼별로 툴이 그래프와 가중치를 어떻게 convert하는 scheme을 구성하는지 수기 변환 매뉴얼을 개발자에게 제공하는게 더 좋을거 같다는 생이 들고 있습니다. ㅎㅎ
칩헤드
2022.10.12 17:32
자작공작소님, 학습할때요, 혹시 모멘텀이랑, Epoch은 어떠셨어요? 제 경우엔 epoch을 올리면 accuracy가 확 올라가는거 같은데, 이게 모델구조 변형시마다 특성이 많이 타더라구요. 다른 분들은 어떻게 반응되는지 궁금해서요. 집단지성이 필요합니다. ^^
칩헤드
2022.10.12 17:36
자작공작소님, 고마움의 표시로, 여기 HactoberFest 참여티켓을 드립니다. 10/31일까지 Level4까지 달성하시면, 영광의 HacktoberFest 뱃지와 티셔츠를 해외택배로 받으십니다. 전 방금 order완료했습니다. https://hacktoberfest.com/
자작공작소
2022.10.12 17:54
옵티마이저를 SDG(momentum=0.9)와 Adam을 사용해 보았는데 Adam이 성능이 훨씬 좋았습니다. epoch를 4~5회만 돌려도 원하는 acc를 얻을 수 있었습니다. Training, validation, test data sets이 동일해서 크게 의미는 없습니다.
칩헤드
2022.10.12 18:27
자작공작소님의 패치를 적용 후 IOT 보드에서 실험을 해보았습니다. 이전과 달리 이번에는 2 outdoor 항목의 인식이 잘 안되고 있습니다.
칩헤드
2022.10.12 18:35
소스코드 빌드시에 기본 옵션 어떻게 하고 쓰세요?, Hard-float ABI, 그리고, newlibc의 빌드시에 [v]Use float with printf from newlib-nano [v]Use float with scanf from newlib-nano 으로 빌드 하셨던거지요?
자작공작소
2022.10.12 18:36
줄이신 버퍼 크기도 원래대로 하셔야 하고 램의 메모리도 어느 정도 확보해야 합니다. 메모리 부족하면 터미널 접속도 바로 바로 되지 않고 여러번 리셋을 해야 됩니다.
자작공작소
2022.10.12 18:40
SENSING1에서 제공하는 컴파일 옵션을 사용하고 있습니다.
자작공작소
2022.10.12 10:25
친절한 설명 감사합니다. 그럼 소스 코드 파일 내의 가중치 버퍼 크기만 변경( #define AI_ASC_DATA_WEIGHTS_SIZE (7840)) 해서 해결 하셨나요? 이것도 임의로 변경하면 컴파일은 되겠으나 모델이 필요로 하는 가중치 값을 제공하지 못해 원하는 결과가 나오지 않을 것으로 생각이 됩니다. 제 경험으로는 ASC_NN_Run()함수를 수정하지 않고서는 FLOAT타입 입출력 파라미터로는 ASC가 동작을 하지 않습니다. 이 방법외에 다른 방법이 있나 해서 궁금한 마음에 문의 드렸습니다. :)
칩헤드
2022.10.12 11:31
자작공작소님, 혹시, 양자화(Quatization)으로 int처리 후에도 현상을 겪으신거에요? 제 경우엔 양자화후엔 음향데이타의 dB 안정화, 모멘텀 튜닝, 모델구조 개선 쪽으로 좀더 탐구했었습니다. 나중에 말씀해주신 부분도 좀더 자세히 관찰하면서 ST 커뮤니티 사람들과 얘길 나눠어야겠습니다. 공유 정말 감사합니다. ^^
자작공작소
2022.10.11 18:53
ASC 사용시 SENSING1 코드에 버그로 인해 수정이 필요한데 수정 없이 해결 하는 방법이 있나요? 저는 이 부분에서 고생을 했습니다.
칩헤드
2022.10.11 23:01
자작공작소님도 이 부분을 만났었군요. 우리가 생성하시는 AI 모델의 그래프 (=네트웍 구조) 정보를 보시면, input featur와 output feature의 Datatype이 있습니다. 이 정보를 토대로 data type을 맞춰주는 작업을 하는게 한가지 방법이었구요. 좀더 시간을 아끼는 다른 편리한(?) 방법중의 하나는 툴체인 개발경험 있는 분들이 접근하는 방법론인데, 우리가 STM32 Cube MX을 이용하여 AI모델을 만들때 정적 라이브러리와 (.a)와 헤드파일들이 같이 생성됩니다. 이 파일들을 STM32 Cube IDE으로 컴파일을 할때 맞추면 소스 빌드시의 에러를 수정하는 엔지니어링 없이도 빌드가 가능하긴 합니다. 이미 경험하셔서 아시겠지만, ST IDE 개발팀에서 흩어져 있는 STM32 AI 구성셋들을 통합하는 개발단계가 아직 성숙되지 않아서, 부득이 TroubleShooting과정을 당분간은 친해져야 하지 않을까? 싶습니다. 이와 관련하여 https://community.st.com 에 세계 도처의 많은 ST 개발보드 사용자분들이 이 부분에서 번뇌하고 계십니다. :)
davinci
2022.09.27 22:43
첫 글 기대가 됩니다 ~~
칩헤드
2022.10.10 14:14
넵, 기대가 크시면, 실망도 크므로, 제가 부담 백배입니다. :)

로그인 후
참가 상태를 확인할 수 있습니다.