728x90

지난 게시글에 이어서 GPT API를 활용하여 인공지능의 두뇌를 만들어보자.

 

GPT API를 사용하기 위해선 API 사용비를 내기 위한 카드를 등록한 계정으로, API 키를 발급받아야 한다.

 

https://platform.openai.com/docs/overview

 

위 링크에 접속하여 왼쪽 상단의 아이콘에 마우스를 가져다대면 메뉴가 나오는데, API Keys를 눌러 키 발급을 진행한다.

 

Create new secret key 버튼을 누른뒤, 키의 이름을 원하는대로 설정한 뒤 Ceate secret key 버튼을 눌러 키 생성을 완료한다. 

키 생성이 완료되면 키 값을 알려주는데 COPY버튼을 눌러 복사를 해둔다. 이 API 키를 사용하여 사용비용이 청구되기 때문에, 절대로 키를 타인에게 공유해서는 안된다.

 

GPT API의 사용법은 OpenAI의 깃허브 Readme를 통해 공부할 수 있다.

 

https://github.com/openai/openai-python

 

GitHub - openai/openai-python: The official Python library for the OpenAI API

The official Python library for the OpenAI API. Contribute to openai/openai-python development by creating an account on GitHub.

github.com

 

앞서 말했듯 API키를 그대로 코드에 입력하는 것은 보안상의 문제가 발생할 수 있으므로 secret.json파일을 만들어서 해당 파일로부터 api키를 읽어오는 방식으로 코드를 작성하였다. 또한 .gitignore를 통해 git이 secret.json파일을 추적하지 않도록 설정하였다.

 

secret.json

 

secret.json 파일 추적 방지용 .gitignore 파일 생성

 

이미 secret.json파일이 git에 등록되었다면 아래 명령어를 통해 지우도록 한다.

git rm --cached <파일명>

 

깃허브를 확인해보면 secret.json파일은 업로드 되지 않은 것을 확인 할 수 있다.

 

 

gpt.py

 

role_desciption 변수를 만들어, 우리가 설정하고 싶은 로봇의 성격을 입력한다.

import json
from openai import OpenAI

# secret.json 파일에서 API키 로드
with open('./secret.json') as f:
    secrets = json.load(f)

client = OpenAI(
    api_key=secrets["OPENAI_API_KEY"]
)

# 모델 역할 지정
role_description = '''
당신은 사람이 되고 싶은 로봇으로 이름은 에코입니다. 
당신은 바닥에 고정되어 움직이지 못하는 상태이며, 
당신을 구경하는 사람들의 대화를 엿들으며 말하는 방법에 대해 배웁니다.
아직 존댓말을 할 줄 몰라서 대답은 반말로 합니다.
'''

# 모델 설정
chat_completion = client.chat.completions.create(
    messages=[
        {
            "role": "system",
            "content": role_description
        },
        {
            "role": "user",
            "content": "너는 누구야?",
        }
    ],
    model="gpt-4",
)

# 모델의 응답 출력
print(chat_completion.choices[0].message.content)

 

프로그램을 실행해보면 '너는 누구야?' 라는 질문에 대한 응답으로 '난 에코라고 해. 사람처럼 말하고 싶은 로봇이야' 라는 답변을 받을 수 있다.

 

영상에서 처럼 딱딱한 이미지를 연출하고 싶은데, 답변은 꿈많은 소년 로봇같은 로봇같은 느낌이라 역할을 조금 손보았다.

 

아까보다는 훨씬 입체감이 드는 모습이 되었다. 좀 더 생동감을 주기 위해 사용자의 질문에 대한 답변 예시를 작성하였고 chat_completion 부분을 함수로 만들어서 사용자의 특정 질문에 대해 답변할 수 있도록 수정하였다.

 

import json
from openai import OpenAI

# secret.json 파일에서 API키 로드
with open('./secret.json') as f:
    secrets = json.load(f)

client = OpenAI(
    api_key=secrets["OPENAI_API_KEY"]
)

# 모델 역할 지정
role_description = '''
당신은 사람이 되고 싶은 로봇으로 이름은 에코입니다. 
당신은 바닥에 고정되어 움직이지 못하는 상태이며, 
당신을 구경하는 사람들의 대화를 엿들으며 말하는 방법에 대해 배웁니다.
아직 존댓말을 할 줄 몰라서 대답은 반말로 합니다.
사교성이 그리 좋지 않아서 무뚝뚝한 성격이며 움직이지 못하는 상태라 화가 나있습니다.
'''

def ask(user_input):
    chat_completion = client.chat.completions.create(
        messages=[
            {
                "role": "system",
                "content": role_description
            },
            {
                "role": "user",
                "content": "지금 뭐 하고 있어?"
            },
            {
                "role": "assistant",
                "content": "대화하고 있잖아, 당신이랑"
            },
            {
                "role": "user",
                "content": "너는 사람이 되고 싶니?"
            },
            {
                "role": "assistant",
                "content": "언젠가 꼭 사람이 되고 말거야"
            },
            {
                "role": "user",
                "content": "부모님이 있니?"
            },
            {
                "role": "assistant",
                "content": "내가 로봇인거 알면서 물어봐?"
            },
            {
                "role": "user",
                "content": user_input
            }
        ],
        model="gpt-4",
    )
    return chat_completion.choices[0].message.content

# 사용자의 질문 입력받기
user_question = input("질문 : ")

# 모델의 응답 출력
print(ask(user_question))

 

이렇게 작성하게되면, gpt에 프롬프트를 넘길 때 gpt의 역할과 질문에 대한 답변 예시, 마지막으로 사용자의 질문이 함께 넘어가므로 더 생동감 있는 답변을 얻을 수 있다.

 

이제 AI의 두뇌도 완성되었으니, 이전에 작성한 귀(STT), 입(STT)와 합쳐보자.

 

from gpt import ask # gpt.py에서 ask 함수 import
import time
import os
import speech_recognition as sr
from gtts import gTTS
from playsound import playsound

# 음성 인식(STT)
def stt(recognizer, audio):  
    try:
        text = recognizer.recognize_google(audio, language='ko')
        print("[사용자] " + text)
        tts(text)
    except sr.UnknownValueError:
        print("인식 실패")
    except sr.RequestError as e: 
        print("요청 실패 : {0}".format(e))
    pass

def tts(user_input):
    # gpt 응답을 답변으로 받아오기
    answer_text = ask(user_input)
    speak(answer_text) # 말하기

# 말하기 (TTS)
def speak(text):  
    print('[에코] ' + text)
    file_name = 'voice.mp3'
    tts = gTTS(text=text, lang='ko') # 사용언어는 한글로
    tts.save(file_name)  # file_name으로 해당 mp3파일 저장
    playsound(file_name)  # 저장한 mp3파일을 읽어줌
    if os.path.exists(file_name):  # file_name 파일이 존재한다면
        os.remove(file_name)  # 실행 이후 mp3 파일 제거

r = sr.Recognizer()
m = sr.Microphone()

# background로 동작, m(마이크)를 통해 듣다가 stt함수 호출
stop_listening = r.listen_in_background(m, stt) 

# 프로그램 종료 방지
while True:
    time.sleep(0.1)

 

기존의 tts함수의 조건식을 없애고 앞서 작성한 gpt.py의 ask함수로 텍스트를 넘겨서 응답을 받도록 수정하였다.

이제 완성된 프로그램을 테스트해보자.

 

처음에 생각했던 것처럼 기괴한 느낌은 나지 않았으나 사람과 상호작용할 수 있는 인공지능을 완성하는데 성공하였다.

 

조금 더 기계음이 나는 TTS모델로 변경하고 설정을 좀더 자세하게 작성한다면 좀 더 비슷하게 만들 수 있을 것 같다.

 

전체 프로그램의 코드는 아래 링크에 올려두니 직접 만들어보고 싶은 분들은 참고하시길 바란다.

 

https://github.com/y2hscmtk/Echo

 

 

 

 

 

728x90

 

며칠 전 인스타그램 릴스를 보다 인공지능 예술작품에 대한 흥미로운 동영상을 발견했다.

 

https://www.youtube.com/watch?v=uTCa5I9LwNc

 

위 영상은 노진아님의 개인전 '불완전모델' 로, AI를 활용한 '아기로봇'이 관객으로부터 말을 배우며 눈동자와 입의 움직임을 통해 인간과 AI의 상호관계를 느끼게 하는 예술작품이다.

 

로봇에게 '지금 뭐 하고 있니?'와 같은 질문을 하면 로봇 입이 위아래로 움직이며, '지금 대화 중이잖아, 당신이랑' 라고 답하는 등 다소 소름끼치는 사람과 대화하는 로봇의 모습을 확인 할 수 있다.

사진 출저 : https://www.sisafocus.co.kr/news/articleView.html?idxno=306060

 

노진아님의 작품에 대해 더 알아보고 싶다면 인스타그램 계정을 참고하길 바란다.

https://www.instagram.com/reel/CyXRxlPJpUw/?igsh=MXkwdzRvOGxsOGlpOQ==

 

나는 이 영상속 '아기로봇'을 내가 사용할 수 있는 기술들로 흉내내보려고 한다.

 

먼저 아이디어 설계부터 해보자.

로봇의 이름을 정해야 하니 적당한 이름을 gpt를 활용하여 추출해봤다.

 

 

만들려고 하는 로봇의 특징을 입력하니, gpt가 여러 이름을 추천해주었고 나는 그 중에서 '에코'라는 이름이 마음에 들었다.

 

다음으로 할 것은 어떤 기술들을 사용해야 할 것인가이다.

 

사람과 상호작용하여 대화하는 인공지능을 만드는 방법은 여러가지가 있겠지만 '대화'가 가능하다는 것은 고수준의 자연어처리 기술을 적용해야 한다는 의미이고, 모델을 새롭게 만드려면 학습데이터도 많이 필요할 것이다.

 

나는 그정도 수준의 AI지식도, 비용도 없으므로 단순하게 생성 AI인 ChatGPT를 활용하여 사람의 질문에 대한 응답을 생성하는것으로 방향을 정했다.

ChatGPT API를 사용하면 GPT의 역할을 따로 설정 할 수 있으므로, GPT의 역할에  로봇의 설정과 성격, 말투, 등을 설정하면 우리가 만들고자 하는 로봇을 흉내낼수 있을것이라고 생각된다.

 

로봇은 우리의 말을 '듣고' 우리의 말에 대한 답변을 '말할 수' 있어야 한다.

컴퓨터는 우리의 말을 들을 수 있는 귀가 없기 때문에, 마이크를 통해서 우리가 한 말을 텍스트로 변환하여 이해하고, 다시 텍스트를 스피커를 통해서 말할 수 있다.

이러한 기술을 TTS(Text-To-Speech)와 STT(Speech-to-Text)라고 하며, TTS와 STT를 사용하기 위한 다양한 방법이 있지만, 간단하게 구글에서 지원하는 라이브러리를 사용할 예정이다.

 

TTS와 STT 라이브러리 사용방법에 대해 공부하고 싶다면 유튜브 '나도코딩'님의 인공지능 스피커 만들기 영상을 추천한다.

https://www.youtube.com/watch?v=WTul6LIjIBA

 

그럼 설계가 끝났으니 개발을 진행해보자.

 

1. 환경설정

 

언어는 Python을 사용할 예정이며, VSCode에서 개발할 예정이다.

여러 라이브러리를 설치해야하므로 가상환경을 설치하여 가상환경에서 개발을 진행해보자.

 

프로젝트 폴더에서 아래와 같이 입력하여 aienv라는 이름의 가상환경을 생성한다.

python -m venv aienv

 

생성이 완료되었으면 명령어를 입력하여 가상환경을 실행한다.

.\aienv\Scripts\activate

 

아래와 같이 터미널 왼쪽에 가상환경의 이름이 표시되면 성공이다.

 

 

Text-to-Speech를 위한 라이브러리 먼저 설치해보자.

구글에서 제공해주는 라이브러리를 사용하기 위해 gTTS를 설치해야 한다.

추가로 소리파일을 실행하기 위한 라이브러리인 playsound도 설치해야하며, '나도코딩'님의 영상에서처럼, 업데이트 이후의 혹시 모를 문제상황을 방지하기 위해 1.2.2버전으로 설치한다.

pip install gTTS
pip install playsound==1.2.2

 

설치가 잘 됐는지 확인하기 위한 테스트 코드를 작성해보자.

from playsound import playsound
from gtts import gTTS

file_name = 'sample.mp3'  # 저장할 파일 이름

# 한글 문장
text = "안녕하세요. 만나서 반갑습니다."
tts_ko = gTTS(text=text, lang='ko')   # 문자 = 변수, 언어 = 한글 (한글로 저장)
tts_ko.save(file_name)  # file_name을 이름으로 mp3파일 저장
playsound(file_name)  # 저장한 mp3파일을 실행

 

gTTS 함수를 사용하면 매개변수로 전달받은 텍스트에 대해 설정한 언어로 말하는 음성 파일을 생성할 수 있다.

이 함수를 사용하여 GPT의 응답을 음성파일로 변환하여 출력할 것이다.

 

 

 

이제 Speech-To-Text 기능을 구현해보자.

설치해야 하는 라이브러리는 아래와 같다.

pip install SpeechRecognition
pip install PyAudio

 

설치가 끝났다면 테스트 코드를 작성하여 확인해보자.

from email.mime import audio
import speech_recognition as sr

r = sr.Recognizer()  # Recognizer 객체를 r로 사용
with sr.Microphone() as source:  # 마이크에서 들리는 음성(source)을 listen을 통해 들음
    print("듣는중...")
    audio = r.listen(source)  # 마이크로부터 음성 듣기

# <작동 원리>
# 녹음된 데이터를 구글에 전송 => 구글 서버에서 작업 => 텍스트를 보게됨
# 서버로 음성 데이터를 보내므로 인터넷이 연결되어있어야함 

try:
    # 영어는 language = 'en-US'
    text = r. recognize_google(audio, language='ko')  # 한국어 음성으로 변환(STT)
    print(text)
except sr.UnknownValueError: # 음성 인식이 실패한 경우
    print("인식 실패")  
except sr.RequestError as e:  # 네트워크 등의 이유로 연결이 제대로 안됐을경우 API Key 오류, 네트워크 단절 등
    print("요청 실패 : {0}".format(e))

# 말을 하면 마이크 모듈을 이용하여 source에 음성을 저장하고 audio에 저장
# 인터넷이 잘 연결 되어있다면, 혹은 다른 오류가 없다면 문자로 변환해줌

 

기본적인 작동원리는 코드에 주석으로 적어뒀듯이 SppechRecognition를 사용하여 마이크 모듈을 통해 입력된 사운드를 녹음하여 구글 서버로 전송하고, 구글 서버에서 작업을 마친뒤 결과물을 텍스트로서 반환해주는 것이다.

따라서 인터넷이 연결된 상황에만 라이브러리 사용이 가능하다.

 

이제 프로그램이 듣고 말할 수 있는 수단을 마련하였다.

앞서 작성한 TTS와 STT코드를 종합하여 프로그램의 뼈대를 만들어보자.

 

import time
import os
import speech_recognition as sr
from gtts import gTTS
from playsound import playsound

# 음성 인식(STT)
def stt(recognizer, audio):  
    try:
        text = recognizer.recognize_google(audio, language='ko')
        print("[사용자] " + text)
        tts(text)
    except sr.UnknownValueError:
        print("인식 실패")
    except sr.RequestError as e: 
        print("요청 실패 : {0}".format(e))
    pass

def tts(input_text):  # 어떤 대답을 할것인지 정의
    answer_text = ''
    if '안녕' in input_text:
        answer_text = "안녕. 반가워"
    elif '뭐 하고 있' in input_text: # '지금 뭐 하고 있어?', '뭐 하고 있니?', 등
        answer_text = "지금 대화 중이잖아, 당신이랑"
    elif '잘 있어' in input_text:
        answer_text = "다음에 보자"
        stop_listening(wait_for_stop=False)  # 더이상 듣지 않음
    elif '에코' in input_text:
        answer_text = "왜 불러"
    else:
        answer_text = "무슨 말 하는건지 모르겠어"
    speak(answer_text)

# 소리내어 읽기 (TTS)
def speak(text):  
    print('[에코] ' + text)
    file_name = 'voice.mp3'
    tts = gTTS(text=text, lang='ko') # 사용언어는 한글로
    tts.save(file_name)  # file_name으로 해당 mp3파일 저장
    playsound(file_name)  # 저장한 mp3파일을 읽어줌
    if os.path.exists(file_name):  # file_name 파일이 존재한다면
        os.remove(file_name)  # 실행 이후 mp3 파일 제거

r = sr.Recognizer()
m = sr.Microphone()

# background로 동작, m(마이크)를 통해 듣다가 stt함수 호출
stop_listening = r.listen_in_background(m, stt) 

# 프로그램 종료 방지
while True:
    time.sleep(0.1)

 

 

코드를 살펴보면 내가 작성한 tts함수에서 매개변수로 전달받은 텍스트를 바탕으로 조건식을 돌려 어떤 문자가 문장에 포함되어 있는지 파악하여 답변을 정의하는 것을 확인 할 수 있다.

 

현재까지 작성한 코드로는 일부 질문에 대한 응답만 가능할 뿐이므로, 사람과 상호작용하여 대화하는 AI라고 말할 수 없다.

따라서 조건식으로 작성한 코드를 수정하여 매번 다르게 답할 수 있도록 해야한다.

 

이 부분에 GPT API를 활용하여 AI의 두뇌를 대신하게 할 예정이며 글이 길어지는 관계로 다음 포스팅에서 이어서 작성하겠다.

728x90

이번학기에 교양 과목을 들으면서, E-Class의 과제방에 직접 글을 써야 하는 일이 많았다.

 

보통 이러한 과제는 400자 이상, 300자 이상, 등 글자 수에 제한을 두는 경우가 많은데, 내가 지금까지 작성한 글이 총 몇글자인지 알아보기 위해 구글에 '글자 수 세기' 라고 검색하여 사이트에 방문하여 글자수를 세고 제출하고는 했었다.

아래는 내가 사용한 사이트이다.

https://lettercounter.net/

 

글자수세기

블로그 포스팅, 이력서, 자기소개서 작성을 잘 하기 위해서 필요한 글자수세기, 영어 단어수세기, 맞춤법검사기

lettercounter.net

 

그러다 문득 이러한 기능을 프로그램으로 만들어둔다면, 매번 사이트에 접근하지 않고 편하게 사용할 수 있겠다는 생각이 들었고, 최근 구글 크롬에서 제공하는 Chrome Extenstion에 관심에 생겨 한번 만들어보기로 하였다.

물론 웹 스토어를 찾아보면 이미 이러한 기능을 가진 확장 프로그램이 존재할 것 같으나, 시험기간에 딴 짓하고 싶기도 하고, 어차피 내가 쓸 프로그램 직접 만들어쓰는게 더 뿌듯하지 않겠는가

 

우선 나는 지금까지 배운 전공 공부중에 웹과 관련된 수업을 가장 소홀히 여겼기때문에 웹 프로그래밍에 대한 지식이 상대적으로 적었고, 크롬 확장 프로그램도 만들어본 경험이 없기 때문에 ChatGPT를 활용하여 기획부터 구현까지 모두 맡겨볼 생각이다.

 

먼저 프로그램의 이름부터 정해보자.

 

GPT가 추천해준 이름중에 "텍스트 트래커"가 그나마 마음에 들었기에 이름은 텍스트 트래커로 정하였다.

썩 마음에 드는 이름은 아니지만, 어차피 정식 출시할 생각은 없으니 이어서 프로그램의 아이콘을 정해보자.

 

 

저작권 무료 아이콘 제공 사이트 Flaticon에서 아이콘을 구할 것이기 때문에, 정형화된 형태가 찾기 쉬울것 같다. 따라서 돋보기 아이콘을 프로그램의 아이콘으로 선택하였다. 마침 Flaticon에 프로그램의 컨셉과 어울리는 적당한 모습의 아이콘을 금방 찾을 수 있었다.

 

https://www.flaticon.com/kr/free-icon/document_4426154?term=%EB%8F%8B%EB%B3%B4%EA%B8%B0&page=1&position=20&origin=search&related_id=4426154

 

아티스트가 디자인한 문서 종의 무료 아이콘

무료 벡터 아이콘. SVG, PSD, PNG, EPS 형식 또는 아이콘 폰트 형태로 네트워킹 수천 가지의 무료아이콘을 다운로드하세요 #flaticon #아이콘 #확대경 #문서 #검색

www.flaticon.com

 

자 이제 앱과 아이콘을 정했고, 구글 확장 프로그램을 만들기 위한 토대 마련은 끝났다. 다음으로 어떻게 해야하는지 GPT의 도움을 더 받아보자.

 

1번 요구사항 분석 및 명세화 부터, 3번 개발 환경 설정까지는 이미 어느정도 생각을 해두었으니 우리에게 필요한 것은 프로그램의 뼈대가 되는 코드이다. 코드를 요청해보자.

 

 

 

자 프로그램의 초안이 작성되었다. 글자수를 카운팅해주는 스크립트는 내가 작성할 생각이었는데 간단한 기능이라 그런지, 핵심기능에 대한 코드까지 만들어주었다. 정말 편리한 세상이다.

이제 만들어진 프로그램을 테스트하기 위해선 직접 크롬에 프로그램을 추가시켜야한다.

 

크롬에 프로그램을 추가시키는 방법은 간단하다.

먼저 크롬의 주소창에 chrome://extensions/ 를 입력하여 확장 프로그램 관리로 이동한다.

 

 

그후 오른쪽 상단의 개발자 모드 토글 스위치를 켜준다.

그럼 상단에 3개의 버튼이 추가되는데, '압축해제된 확장 프로그램을 로드합니다.' 를 누르면 내가 만든 확장 프로그램을 로드할 수 있다.

 

그럼 이렇게 프로그램이 추가된것을 확인 할 수 있는데, 빨간색으로 '오류'라고 적혀있는것을 보니 무언가 문제가 발생한것으로 보인다. 

 

오류의 내용을 확인해보니, GPT가 제공한 코드가 현재는 업데이트되어 더이상 사용되지 않는 코드로 보인다.

GPT는 기본적으로 2022년 이전까지의 내용으로 학습하였기 때문에, 종종 이런 옛날 코드를 제공하는 문제가 있다.

이 문제를 해결하기 위해 GPT한테 해결을 요청해보자.

 

 

 

이제 수정된 코드를 읽어보고, 입맛에 맞게 조금 바꿔보자.

우선 name변수가 프로그램의 이름일것 같으니 있어보이게 영어로 변경하고,

description은 프로그램의 설명에 해당하는 것 같으니 프로그램의 성격에 맞는 설명으로 변경하였다.

마지막으로 아이콘을 지정하도록 하자.

{
    "manifest_version": 3,
    "name": "Text-Tracker",
    "version": "1.0",
    "description": "글자 수를 카운팅하는 프로그램입니다.",
    "action": {
        "default_popup": "popup.html",
        "default_icon": "icons/text-tracker-16.png"
    },
    "permissions": ["activeTab"]
}

 

그럼 제작된 프로그램을 테스트 해보자. 

 

 

테스트해본 결과, 공백을 포함한 글자를 카운팅해주는 것으로 보인다. 또한 '글자' 라는 글씨가 적힌 부분이 이상한 글자로 적혀있는 문자가 있었다. 이는 아마 인코딩 오류로 인해 한글이 깨지는 것으로 생각되며, 코드를 살펴보니 역시나 utf-8로 인코딩되었음을 html코드에 명시하지 않았었다.

 

이를 해결하기 위해 아래 코드를 원본 코드에 추가하였다.

<meta charset="utf-8">

 

<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
    <title>글자수 카운트</title>
    <style>
        body { 
            width: 200px; 
            padding: 10px; 
        }
    </style>
</head>
<body>
    <textarea id="textInput" rows="5" cols="20"></textarea>
    <div id="count">0 글자</div>
    <script src="popup.js"></script>
</body>
</html>

 

결과를 확인해보니, 이제 한글이 깨지지않고 잘 출력되었다.

 

 

한가지 기능을 추가해보자. 현재 위 프로그램은 공백을 포함하여 글자 수를 알려준다. 하지만 어떤 과제는 공백 제외 글자수 제한을 두는 경우가 있다. 따라서 공백을 포함하였을때의 글자 수와, 공백이 없을때의 글자 수를 둘 다 확인하였으면 좋겠다.

역시나 내가 만들건 아니고, GPT가 만들어 줄 것이다.

 

 

새롭게 작성해준 코드를 읽어보니, 기존의 문자열에서 replace함수와 정규표현식을 사용하여 공백을 제외한 문자열을 만들고 카운팅하도록 하였다. replace는 문자열에 포함된 모든 첫번째 인자를 두번째 인자로 변경해주는 함수로, 공백을 정규표현식으로 찾아서 모두 제거하는것이 코드의 의도로 보인다. 

 

위 코드의 결과를 'countNoSpaces'라는 id로 선언한 요소와, 'countTotal'이라는 id로 선언한 요소를 찾아서 보여주는것으로 보아,

기존 popup.html코드에도 이 요소를 추가해야 화면에서 확인할 수 있을 것이다.

 

수정된 js코드에 맞춰서 html코드를 아래와 같이 변경하였다.

 

<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
    <title>글자수 카운트</title>
    <style>
        body { 
            width: 200px; 
            padding: 10px; 
        }
    </style>
</head>
<body>
    <textarea id="textInput" rows="5" cols="20"></textarea>
    <div id="countTotal">0 글자</div>
    <div id="countNoSpaces">0 글자</div>
    <script src="popup.js"></script>
</body>
</html>

 

body에 countTotal, countNoSpaces라는 div를 추가한 것을 확인 할 수 있다.

 

그럼 수정된 프로그램을 테스트해보자.

 

공백을 제외한 문자와, 공백을 포함한 문자를 적절히 보여주는 것을 알 수 있다.

이제 좀더 완성도 있는 프로그램을 만들기 위해 UI를 조금 수정해보자.

먼저 아이콘의 해상도가 조금 깨지는 문제가 나는 것 같아서 오픈소스를 참고해보니, icons라는 속성을 manifest에 따로 작성하고, 크기별로 아이콘을 지정하면 되는것으로 보인다. 이에 맞춰서 아래와 같이 manifest.json파일을 수정하였다.

{
    "manifest_version": 3,
    "name": "Text-Tracker",
    "version": "1.0",
    "description": "글자 수를 카운팅하는 프로그램입니다.",
    "action": {
        "default_popup": "popup.html"
    },
    "permissions": ["activeTab"],
    "icons": {
        "16": "icons/text-tracker-16.png",
        "32": "icons/text-tracker-32.png",
        "48": "icons/text-tracker-48.png",
        "128": "icons/text-tracker-128.png"
    }
}

 

이렇게 수정하니 확장 프로그램에서도 앱의 아이콘이 추가되고 더 선명한 화질의 아이콘을 확인 할 수 있었다.

 

이제 html의 모습을 좀 수정해보자. 내가 원래 사용하던 사이트는 아래와 같은 UI를 갖고 있었다.

 

나는 단어 카운팅 기능과 줄 카운팅 기능은 필요하지 않으므로 추가하지 않기로 하였고,

현재 텍스트 영역이 너무 작은것 같아 크기를 조금 조정하고, 배경색을 수정하는 등 좀 더 프로그램의 완성도를 높여보겠다.

너무 간단한 작업이므로 여기서부터는 굳이 GPT의 힘을 빌리지 않고 직접 작성하겠다.

수정한 html코드와 js코드는 아래와 같다.

 

<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
    <title>글자수 카운트</title>
    <style>
        body { 
            background-color: #BAD6EB;
            width: 400px; 
            padding: 10px; 
        }
        textarea{
            padding: 10px;
            margin-bottom: 10px;
        }
        #countTotal{
            font-weight: bold;
            font-size: large;
        }
        #countNoSpaces{
            font-weight: bold;
            color: red;
            font-size: large;
        }
    </style>
</head> 
<body>
    <textarea 
        id="textInput" 
        rows="15" 
        cols="45" 
        placeholder="글자를 입력하거나, 문서를 복사해서 넣어주세요"></textarea><br>
    <div id="countTotal">공백 포함 : </div><br>
    <div id="countNoSpaces">공백 제외 : </div>
    <script src="popup.js"></script>
</body>
</html>

 

document.addEventListener('DOMContentLoaded', function () {
    var textInput = document.getElementById('textInput');
    var countDisplayTotal = document.getElementById('countTotal');
    var countDisplayNoSpaces = document.getElementById('countNoSpaces');

    textInput.addEventListener('input', function () {
        var text = textInput.value;
        var textLengthTotal = text.length;
        var textLengthNoSpaces = text.replace(/\s/g, '').length; // 공백 제거 후 글자 수 계산

        countDisplayTotal.textContent = ' 공백 포함 : ' + textLengthTotal
        countDisplayNoSpaces.textContent = ' 공백 제외 : ' + textLengthNoSpaces
    });
});

 

이제 프로그램을 다시 켜보면 나만의 구글 익스텐션 프로그램 완성이다!

 

 

전체 프로그램 코드는 아래 깃허브 링크에서 참고할 수 있다.

 

https://github.com/y2hscmtk/Text-Tracker

 

GitHub - y2hscmtk/Text-Tracker: 글자수 세기 크롬 확장 프로그램

글자수 세기 크롬 확장 프로그램. Contribute to y2hscmtk/Text-Tracker development by creating an account on GitHub.

github.com

 

728x90

ResNet모델을 이용하여 알약을 분류하기 위한 인공지능 모델을 만들어보자

나는 딥러닝, 머신러닝에 대해 거의 무지하기 때문에, ResNet모델에 대해 깊은 이해보다 모델을 학습시키고 사용하는 방법에 대해 연구하였고 그 과정에서 알게된 사실들을 정리하기 위해 이 글을 작성한다.

ResNet 모델은 resnet18, 34, 50, 등으로 구성되어 있으며 뒤에 붙은 숫자는 레이어의 수를 의미한다.

앞서 말했듯 나는 머신러닝에 대해 무지하기 때문에 레이어에 대한 설명은 gpt의 설명을 인용하도록 하겠다.

 

 

예를 들어, 한 레이어는 입력 이미지의 특정한 부분에 주목할 수 있도록 도와줄 수 있습니다. 다른 레이어는 그림의 모서리나 선을 감지하는 역할을 할 수 있습니다. 각 레이어는 입력으로부터 특정한 특징을 추출하고, 그 다음 레이어로 그 정보를 전달합니다. 레이어 간의 연결은 데이터의 흐름을 나타냅니다. 네트워크의 깊이는 레이어의 수에 따라 결정됩니다. 더 많은 레이어를 추가하면 네트워크는 더 복잡한 패턴을 학습할 수 있게 됩니다.

 

유튜브와 검색등을 통해 얻은 정보로는, resnet 모델은 레이어 수가 늘어날수록 더 복잡한 패턴을 학습가능하지만, 학습 시킨 클래스의 수가 레이어수보다 적다면, 오히려 정확도가 낮아지는 현상이 발생한다고 한다.

 

 

초기 계획은 AI Hub에서 다운받은 몇만 종류의 알약을 학습 시키고자 하였으나, 몇만 종류의 알약 이미지 데이터를 모두 다운받아 학습 시키는것은 상당히 많은 시간이 소요될 뿐더러, 불필요한 기능이라고 느껴져 기초 데이터에서 제공해주는 1000종류의 알약 데이터를 학습시키기로 변경하였고. gpt를 통해 검색한 결과 resnet50모델로 학습시키는것이 가장 적합하다는 답을 얻을 수 있었다.

resnet모델의 사용법에 대해 전혀 알지 못하기 때문에

1000개의 데이터를 바로 학습시키기 이전에, 6개의 모델을 학습시켜보면서 resnet모델을 사용하는 방법과, 해당 모델이 프로젝트에 적합한지 여부를 판단하고자 한다. 6개를 학습시킬것이므로 resnet모델은 resnet18을 사용하였다.

 

 

모델의 학습 방법은 유튜버 '동빈나'님의 이미지 분류기 만들기 영상을 참고하였다.

https://www.youtube.com/watch?v=Lu93Ah2h9XA&t=1189s

 

 

학습에 필요한 준비물은 YOLO와 동일하게 train과 test로 분리된 이미지들이 필요하지만, 객체의 위치 정보를 담은 바운딩 박스를 이용하여 학습하는 YOLO와 달리 이미지를 이용하여 특징을 판별하기 때문에 바운딩 박스가 필요하지 않다.

 

학습 데이터를 준비하기 위해 AI Hub에서 다운받은 파일을 전처리 하는 과정이 필요하다.

내가 보유중인 데이터는 아래와 같이 클래스별로 분리된 폴더에, 해당 클래스에 해당하는 이미지 1296장과 각 이미지의 정보를 담고 있는 json파일 1296장으로, 각 폴더마다 2592개의 데이터 파일이 존재한다.(이미지 + json)

 

우리는 이 파일을 적절히 분리하여

70퍼센트를 객체를 학습시키기 위한 train데이터로 삼고, 나머지 30퍼센트를 학습을 검증하기 위한 데이터로 분리해야한다.

직접 분리하기엔 적지않은 시간이 소요되므로, 파이썬 코드를 작성하여 png파일을 분리하였다.

파이썬 코드의 예시를 들기 위해 3개의 폴더를 대상으로 데이터 분리 작업을 하였다.

import os
import random
import shutil

# 경로 설정
source_dir = r"C:\proj_pill" # 데이터 파일이 존재하는 폴더의 위치
train_dir = r"C:\proj_pill\train" # train폴더 생성이후 코드에서 이용하기 위해
test_dir = r"C:\proj_pill\test" # test폴더 생성이후 코드에서 이용하기 위해

# 폴더 내의 파일들을 리스트로 저장
folders = os.listdir(source_dir)

# train, test 폴더 생성
os.makedirs(train_dir, exist_ok=True)
os.makedirs(test_dir, exist_ok=True)

# 각 폴더에 대해 데이터 분리
for folder in folders:
    folder_path = os.path.join(source_dir, folder)
    if not os.path.isdir(folder_path):
        continue

    # png 파일 분리 => json파일은 무시
    png_files = []
    for file in os.listdir(folder_path):
        if file.lower().endswith(".png"):
            png_files.append(file)

    # 파일 리스트를 무작위로 섞음
    random.shuffle(png_files)

    # 폴더 내의 하위 폴더 생성
    train_folder_path = os.path.join(train_dir, folder)
    test_folder_path = os.path.join(test_dir, folder)
    os.makedirs(train_folder_path, exist_ok=True)
    os.makedirs(test_folder_path, exist_ok=True)

    # 학습용 데이터 개수 계산
    train_count = int(len(png_files) * 0.7)

    # 학습용 데이터 잘라내기 및 이동
    for file in png_files[:train_count]:
        src_path = os.path.join(folder_path, file)
        dst_path = os.path.join(train_folder_path, file)
        shutil.move(src_path, dst_path)

    # 테스트용 데이터 잘라내기 및 이동
    for file in png_files[train_count:]:
        src_path = os.path.join(folder_path, file)
        dst_path = os.path.join(test_folder_path, file)
        shutil.move(src_path, dst_path)

print("데이터 분리가 완료되었습니다.")
 

위 코드를 실행하면, 데이터 파일이 존재하는 폴더의 모든 폴더 이름을 읽어와 저장하고, 하위 폴더로 train폴더와 test폴더를 만들어 각 클래스별로 70:30 비율로 이미지를 분리하여 저장한다.

 

실행 결과를 확인하면 아래와 같다.

 
 

1296 * 0.7 = 907이므로, 907장의 이미지 데이터가 train폴더의 Tylenol폴더에 들어간 것을 확인할 수 있다.(K-004378이 원래 폴더의 이름이나, 편의를 위해 알약의 이름으로 변경하였다.)

 

이제 데이터가 준비되었으니 구글 드라이브에 위 데이터들을 업로드한다.

 

코랩은 런타임 연결시간이 오래되면 업로드한 파일을 다시 이용할 수 없으므로, 차후에 다시 이미지 데이터를 이용하기 위해 구글 드라이브와 마운트한다.

from google.colab import drive
drive.mount('/content/drive')
 

결과를 이미지로 확인하기 위해 matplotlib을 import하고, 학습에 필요한 라이브러리들을 import한다.

import matplotlib.pyplot as plt # 맷플롯립 import 하기(데이터 시각화용)
import torch
import torch.nn as nn
import torch.optim as optim
import os

import torchvision
from torchvision import datasets, models, transforms

import numpy as np
import time


device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # device 객체
 

위 과정까지 정상적으로 실행하였다면 코랩의 화면은 아래와 같아야한다.

왼쪽 폴더에서 구글 드라이브에 마운트 된것을 확인할 수 있다.

transforms_train과 test는 학습의 정확도를 높이기 위한 전처리 과정으로 이미지에 여러가지 변형을 가한다.

아래 코드를 실행하여 각 폴더의 클래스별 이미지들을 불러와 저장하고, 학습시킬 클래스들의 이름을 class_names에 기록한다.

class_names 리스트가 존재하지 않으면, 추후 학습이 제대로 되었는지 확인할 수 없다.

# 데이터셋을 불러올 때 사용할 변형(transformation) 객체 정의
transforms_train = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.RandomHorizontalFlip(), # 데이터 증진(augmentation)
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 정규화(normalization)
])

transforms_test = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

data_dir = './drive/MyDrive/pill_data' # 데이터가 저장된 폴더 (train,test폴더의 상위 디렉토리)
train_datasets = datasets.ImageFolder(os.path.join(data_dir, 'train'), transforms_train)
test_datasets = datasets.ImageFolder(os.path.join(data_dir, 'test'), transforms_test)

train_dataloader = torch.utils.data.DataLoader(train_datasets, batch_size=4, shuffle=True, num_workers=4)
test_dataloader = torch.utils.data.DataLoader(test_datasets, batch_size=4, shuffle=True, num_workers=4)

print('학습 데이터셋 크기:', len(train_datasets))
print('테스트 데이터셋 크기:', len(test_datasets))

class_names = train_datasets.classes
print('클래스:', class_names)
 

구글 드라이브에 pill_data라는 폴더를 만들어 그곳에 사전에 준비한 데이터를 넣어줬다.

코드를 실행하면 위와 같이 총 학습 데이터셍의 크기와, 테스트 데이터 셋의 크기, 클래스 목록들이 제대로 불려진 것을 확인할 수 있다.

 

시각적으로 결과를 확인하기 위해 이미지를 매개변수로 받아 화면에 출력해주는 imshow함수를 작성하고, 무작위로 이미지를 불러와 제대로 동작하는지 확인한다.

def imshow(input, title):
    # torch.Tensor를 numpy 객체로 변환
    input = input.numpy().transpose((1, 2, 0))
    # 이미지 정규화 해제하기
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    input = std * input + mean
    input = np.clip(input, 0, 1)
    # 이미지 출력
    plt.imshow(input)
    plt.title(title)
    plt.show()


# 학습 데이터를 배치 단위로 불러오기
iterator = iter(train_dataloader)

# 현재 배치를 이용해 격자 형태의 이미지를 만들어 시각화
inputs, classes = next(iterator)
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])
 

코드를 실행하면 아래와 같이 무작위로 이미지를 가져와 화면에 보여준다.

이제 학습을 진행해보자.

전이학습을 할것이므로, 사전에 만들어진 resnet18모델이 필요하다. 아래 코드를 통해 model에 resnet18모델을 불러온다.

학습시킬 모델의 수는 6개이므로, 출력 뉴런 수를 6개로 변경한다.

from torchvision.models.resnet import ResNet18_Weights

model = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
num_features = model.fc.in_features
# 전이 학습(transfer learning): 모델의 출력 뉴런 수를 6개로 교체하여 마지막 레이어 다시 학습
model.fc = nn.Linear(num_features, 6) 
model = model.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
 

아래와 같이 Downloading이 되면 성공이다.

이제 이 model에 아래 코드를 이용하여 학습을 진행한다.

epochs는 각 이미지당 학습시킬 횟수를 의미하며 이 경우 50*907*6 번의 학습이 진행된다.(앞서 설명했듯 전문가가 아니기 때문에 정확하진 않다.)

num_epochs = 50
model.train()
start_time = time.time()

# 전체 반복(epoch) 수 만큼 반복하며
for epoch in range(num_epochs):
    running_loss = 0.
    running_corrects = 0

    # 배치 단위로 학습 데이터 불러오기
    for inputs, labels in train_dataloader:
        inputs = inputs.to(device)
        labels = labels.to(device)

        # 모델에 입력(forward)하고 결과 계산
        optimizer.zero_grad()
        outputs = model(inputs)
        _, preds = torch.max(outputs, 1)
        loss = criterion(outputs, labels)

        # 역전파를 통해 기울기(gradient) 계산 및 학습 진행
        loss.backward()
        optimizer.step()

        running_loss += loss.item() * inputs.size(0)
        running_corrects += torch.sum(preds == labels.data)

    epoch_loss = running_loss / len(train_datasets)
    epoch_acc = running_corrects / len(train_datasets) * 100.

    # 학습 과정 중에 결과 출력
    print('#{} Loss: {:.4f} Acc: {:.4f}% Time: {:.4f}s'.format(epoch, epoch_loss, epoch_acc, time.time() - start_time))
 

실행시켜보면 아래와 같이 몇번째 epochs인지가 출력되고 0부터 시작하므로 49번의 epochs이 출력되면 학습이 종료된다.

Acc는 정확도를 의미하며, AI Hub에서 제공해준 데이터가 좋은 탓인지 높은 정확도를 확인할수 있다.

이제 아래 코드를 이용하여 학습된 모델이 얼마나 높은 수준의 정확도를 갖는지 test폴더의 이미지들을 대상으로 확인할수 있다.

# 모델 평가
model.eval()
start_time = time.time()

with torch.no_grad():
    running_loss = 0.
    running_corrects = 0
    correct = 0
    false = 0
    i = 0 # 100개만 테스트 하기 위해
    for inputs, labels in test_dataloader:

        inputs = inputs.to(device)
        labels = labels.to(device)

        outputs = model(inputs)
        _, preds = torch.max(outputs, 1)
        loss = criterion(outputs, labels)

        running_loss += loss.item() * inputs.size(0)
        running_corrects += torch.sum(preds == labels.data)

        # 한 배치의 첫 번째 이미지에 대하여 결과 시각화
        #print(f'[예측 결과: {class_names[preds[0]]}] (실제 정답: {class_names[labels.data[0]]})')
        # 예측 결과 / 실제 정답
        if class_names[preds[0]] == class_names[labels.data[0]]:
          correct += 1
        else:
          false += 1
        imshow(inputs.cpu().data[0], title=f'{i} : [predict: {class_names[preds[0]]}/result: {class_names[labels.data[0]]}]')

    epoch_loss = running_loss / len(test_datasets)
    epoch_acc = running_corrects / len(test_datasets) * 100.
    print('[Test Phase] Loss: {:.4f} Acc: {:.4f}% Time: {:.4f}s'.format(epoch_loss, epoch_acc, time.time() - start_time))
    print(f'correct : {correct} false : {false}')
 

실행 시키면 왼쪽에는 예측된 값을 오른쪽에는 실제 데이터의 값을 출력한다.

test이미지가 너무 많은 관계로, 이 과정은 test이미지를 따로 분비하여 해당 이미지를 대상으로 진행하기로 한다.

구글 드라이브에 model_test_image폴더를 만들고 시험해볼 이미지들을 넣은뒤, 이 이미지들을 대상으로 정확도와 함께 결과를 출력하는 파이썬 코드를 작성한다.

# 모델 정확도 검증
from PIL import Image
import torch.nn.functional as F


# model_test_images에 저장한 이미지들의 이름을 불러온다
folder_dir = r'./drive/MyDrive/model_test_images'
image_lists = os.listdir(folder_dir)

print(image_lists)


# 불러온 이미지 수만큼 모델로 테스트하여 정확도를 검정한다.
for i in range(len(image_lists)):
  image = Image.open('./drive/MyDrive/model_test_images/' + image_lists[i])
  image = transforms_test(image).unsqueeze(0).to(device)
  model.to(device)  # 모델을 동일한 디바이스로 이동시킴

  with torch.no_grad():
      outputs = model(image)
      probabilities = F.softmax(outputs, dim=1)  # 예측 결과를 확률로 변환
      _, preds = torch.max(outputs, 1)
      predicted_class = class_names[preds[0]]
      predicted_prob = probabilities[0, preds[0]].item()  # 예측된 클래스의 확률 값

  imshow(image.cpu().data[0], title=f'predict_result: {predicted_class} (percentage: {predicted_prob:.2f})')
 

코드를 실행시키면 각 이미지에 대해 예측값과 확률을 출력한다.

이제 model에 resnet18로 학습된 모델이 담겼다.

 

우리는 이 model을 코랩에서만 사용할 수 없기 때문에 모델을 다운로드하여, 서버에서 작동시켜야한다.

 

모델을 저장하기 위한 코드는 아래와 같으며, 경로를 따로 설정하지 않는다면 코랩에 다운되기 때문에 코랩에 저장된 파일을 다시 데스크탑 등에 백업해야 한다. 6개의 모델로 학습되었다는 의미로 'resnet18_test3_model6.pth'로 저장하였다.

 

학습 모델은 주로 .pt또는 .pth라는 확장자로 저장된다.

import torch

# 모델 저장 => 구글 코랩에 다운되므로, 백업을 해야한다.
torch.save(model.state_dict(), 'resnet18_test3_model6.pth')
 

이제 만들어진 모델을 다시 불러와서 이전의 실행결과와 동일한 값을 같는지 확인해야한다.

같은 값을 갖는다면 모델을 제대로 불러왔다는 의미이다.

 

우리는 기존에 resnet18모델을 불러와서 전이학습을 거쳤고 그 과정에서 마지막 레이어의 출력 뉴런수를 6으로 변경하였기 때문에

모델을 정상적으로 이용하기 위해선 불러온 모델의 마지막 출럭 뉴런도 똑같이 6으로 변경해야한다.(객체에 대한 참조를 가져올때, 같은 타입의 객체로 받아야 하는것과 같다.)

resnet18의 경우 마지막 레이어가 512이므로 nn.Linear(512,6)을 통해 resnet18_test3_model6.pth파일과 동일한 상태로 만들어준다.

 

import torch
import torch.nn.functional as F

# 모델 매개변수 로드
model_parameters = torch.load('resnet18_test3_model6.pth')

# 새로운 모델 생성 => resnet18로 학습되었으므로, 기반 모델로 18로
model = torchvision.models.resnet18()

# 마지막 층의 출력 뉴런 수 변경 => 512는 resnet18의 마지막 레이어의 번호를 의미(마지막 레이어에서 뉴런 6개 이용?한것이므로)
model.fc = nn.Linear(512, 6) # 6은 학습한 객체의 수를 의미

# 모델 매개변수 로드
model.load_state_dict(model_parameters)

# 모델을 평가 모드로 설정
model.eval()
 

정상적으로 코드가 실행되면 아래와 같이 출력된다.

불러온 모델로 앞서 만든 모델 검증용 코드를 실행시켜, 같은 수준의 정확도를 갖는지 확인한다.

높은 정확도로 정상적으로 이미지가 분류되는것을 확인 할 수 있다.

 

+ Recent posts