티스토리 뷰
아마도 내가 대학에서 진행하는
마지막 프로젝트이자 고비가 아닐까 싶다.
실패함
들어가며
제목에서도 알 수 있듯이, 이 프로젝트는 실패로 돌아갔습니다. 자연어처리 수업의 일환으로 진행한 이 프로젝트는 새벽마다 스터디카페에 가서 연구하고 제작했지만 결국 모델링 학습에 실패했습니다.
현재 재학 중인 학부는 딥러닝 및 자연어처리 분야와 깊은 연관이 있지만, 입학을 다른 학과로 시작한 탓에 현재 학부에서 요구하는 수준의 지식이 결여된 상태로 시작한 프로젝트입니다. 따라서 저의 글이 큰 도움이 되지는 않을 것 같지만, 언젠가 저와 같은 길 잃은 학부생들에게 동지애를 만들어주고 싶어 이렇게 글을 작성하게 되었습니다.
본래 프로젝트는 중간 회고록도 작성할 계획이었으나, 프로젝트 현황이 노력에 비해 무척 지지부진하여 결국 최종 회고록만 작성하게 되었습니다. 프로젝트가 종료된 후, 이 글을 작성하며 코랩 파일을 다시 뜯어보고 있습니다. 이 코드 하나하나에 담긴 제 노력이 고스란히 기억에 남아있어 혼자 감동하며 이 글을 작성하고 있습니다.🥲
이 프로젝트 회고는, 전혀 거창하지 않은 원숭이 4학년의 NER 실패 기록기입니다.
프로젝트 개요
해당 프로젝트에서는 한국어 NER에 대해 다루고 있습니다. NER(Named Entity Recognition)이란 텍스트에서 인명·지명·기관명·날짜 등 특정 개체명을 식별하는 자연어처리 기술로 감정 분석과 더불어 자연어처리 기본 분야 중 하나입니다. 다만, 한국어 개체명 인식의 경우는 선행자료와 사용할 수 있는 모델이 상당히 적으며 대개 파인튜닝을 사용해 진행해야 하며, 때문에 영어로 진행하면 프로젝트 수행이 훨씬 수월해질 것으로 예상됩니다.
해당 프로젝트는 한국어 말뭉치 데이터를 사용해 Entity를 식별하는 다양한 NER 모델의 성능을 비교하는 것을 목표로 하며, 이를 통해 각 모델의 장단점을 분석하고, 한국어 텍스트 데이터에 가장 적합한 NER 모델을 제안할 것을 목표로 잡았습니다.
데이터는 국립국어원에서 제공하는 개체명 분석 말뭉치를 사용했다.
https://kli.korean.go.kr/corpus/main/requestMain.do
원래는 개체명 인식에 특화된 모델들을 사용할 계획이었으나, 대부분 서비스가 종료되었거나 추가 업데이트가 되지 않아 파인튜닝을 기본으로 수행하게 되었습니다.
선행 연구
해당 프로젝트는 되는대로 많은 선행 연구를 찾아 개체명 인식기의 보편적인 진행 단계를 알고자 하였습니다.
publicservant_AI/5_(BERT_실습)한국어_개체명_인식.ipynb at master · kimwoonggon/publicservant_AI
김웅곤 - 텐서플로우와 케라스로 구현한 NLP 기초 (2020년 버전). Contribute to kimwoonggon/publicservant_AI development by creating an account on GitHub.
github.com
GitHub - ai2-ner-project/pytorch-ko-ner: PLM 기반 한국어 개체명 인식 (NER)
PLM 기반 한국어 개체명 인식 (NER). Contribute to ai2-ner-project/pytorch-ko-ner development by creating an account on GitHub.
github.com
GitHub - eagle705/pytorch-bert-crf-ner: KoBERT와 CRF로 만든 한국어 개체명인식기 (BERT+CRF based Named Entity Recogn
KoBERT와 CRF로 만든 한국어 개체명인식기 (BERT+CRF based Named Entity Recognition model for Korean) - eagle705/pytorch-bert-crf-ner
github.com
그 외 다수의 선행 연구 자료들을 참고했으며, 해당 연구를 위해 사용한 모델은 대부분 깃허브에 배포된 한국어 전용 개체명 인식기를 사용하려고 하였으나 업데이트가 되지 않아 Hugging Face의 한국어 전용 모델을 파인튜닝해 사용하고자 하였습니다.
KPF/KPF-bert-ner · Hugging Face
KPF-BERT-NER 빅카인즈랩 인사이드 메뉴의 개체명 분석에서 사용된 개체명 인식 모델이다. 사용 방법에 대한 안내 및 코드는 KPF-bigkinds github에서 확인할 수 있습니다. 모델 소개 KPF-BERT-NER 한국언론
huggingface.co
kykim/bert-kor-base · Hugging Face
🗣️ reflex-ai/MeloTTS-English-v3 🗣️ neuromod0/MeloTTS-English-v3
huggingface.co
데이터 전처리
OoV 토큰 문제
데이터셋에서 무시하기 힘들정도로 많은 양의 UNK 토큰이 반환되었습니다. UNK 토큰은 문제 해결을 위해 tokens의 구성 방식도 바꿔보았지만, 해결되지 않았습니다. Unknown Token는 새로운 단어 토큰이 vocabulary에 없을 때 반환됩니다. 당연히 부정확하며, 언어 모델에서 치명적으로 작용할 확률이 큽니다. 그 양이 적다면 무시하거나, 수기로 작성해주면 되지만 이 경우에는 너무 많은 토큰이 UNK로 인식되었습니다.
cleaned_sent_form tokens token_ids
0 가장 좋아하다 음식 무엇 [[UNK], [UNK], [UNK], [UNK]] [2, 0, 0, 0, 0, 3]
1 저 [저] [2, 7199, 3]
2 주로 먹다 거 좋아하다 [[UNK], [UNK], 거, [UNK]] [2, 0, 0, 5377, 0, 3]
3 음식 가리다 않다 좋아하다 [[UNK], [UNK], [UNK], [UNK]] [2, 0, 0, 0, 0, 3]
4 맵다 음식 좋아하다 [[UNK], [UNK], [UNK]] [2, 0, 0, 0, 3]
이렇듯, 단어 집합에 존재하지 않는 단어들이 마구 생기는 상황을 Out of Vocabulary라 하여 OoV 토큰 문제라고 칭하게 됩니다. 코드를 이리저리 수정해보았지만, 해결되지 않았고 데이터셋의 문제라고 판단해 기존에 선정한 데이터셋을 폐기했습니다.
조금 오래되었지만 OoV 문제를 반환하지 않는 2019년 데이터셋을 사용하게 되었습니다.
토크나이징 적용
okt = Okt()
def clean_text(text):
if not isinstance(text, str):
return [] # 입력이 문자열이 아니면 빈 리스트 반환
text = re.sub(r'[^\w\s]', '', text) # 문장 부호 제거
tokens = okt.pos(text, stem=True) # 형태소 분석 및 어미 삭제
return tokens
데이터셋에 입력된 문장부호와 형태소, 어미를 삭제해 토큰화를 진행했습니다.
결과
sentence \
0 [횡설수설/권순활]北 ‘외화벌이’ 뜯어먹기
1 필리핀 국민의 약 10%인 800만 명은 세계 곳곳에서 건설노동자 가정부 유모 등으...
2 본국의 가족에게 보내는 송금 총액은 매년 100억 달러를 넘어 필리핀 경제를 지탱하...
3 멕시코 파키스탄 방글라데시 베트남 역시 해외파견 근로자의 송금이 한몫을 한다.
4 한국도 과거 개발연대 시절 서독에 나간 광원과 간호사, 베트남과 중동에 진출한 근로...
tokens
0 [(횡설수설, Noun), (권, Suffix), (순, Modifier), (활,...
1 [(필리핀, Noun), (국민, Noun), (의, Josa), (약, Noun)...
2 [(본국, Noun), (의, Josa), (가족, Noun), (에게, Josa)...
3 [(멕시코, Noun), (파키스탄, Noun), (방글라데시, Noun), (베트...
4 [(한국, Noun), (도, Josa), (과거, Noun), (개발, Noun)...
이 과정에서 expected string or bytes-like object 에러가 발생합니다. 해당 에러는 함수가 기대한 것과 다른 타입의 객체를 전달했을 때 발생합니다.
함수의 타입을 확인해주는 코드를 집어넣어 해결해주었습니다.
if not isinstance(text, str):
return [] # 입력이 문자열이 아니면 빈 리스트 반환
토큰화 결과를 보면 기댓값과 다른 결과값들이 있습니다. 최적화를 위해 보통 수기로 수정하는 것 같습니다만, 저는 일단 다음 단계로 넘어갔습니다.
불용어 사전
sxne_df = pd.DataFrame(sxne_data_list)
# 형태소 분석기 및 불용어 목록
okt = Okt()
korean_stopwords = [
"이", "그", "저", "것", "거기", "여기", "그리고", "그러나", "하지만", "또", "또한", "그러므로", "그러면", "한", "의", "가", "이", "들", "는", "도", "을", "에", "와", "한", "하다"
]
# 형태소 분석 후 불용어 제거 함수
def tokenize_and_remove_stopwords(text, stopwords):
tokens = okt.morphs(text)
tokens = [token for token in tokens if token not in stopwords]
return ' '.join(tokens)
# 형태소 분석 및 불용어 제거 적용
sxne_df['processed_sentence'] = sxne_df['sentence'].apply(lambda x: tokenize_and_remove_stopwords(x, korean_stopwords))
불용어 사전을 만들어주었습니다. 불용이란 말 그대로 필터링을 위한 사전입니다. 문장에 자주 등장하지만 분석에 있어 큰 의미가 없기 때문에 필터링되었음 하는 것들을 모아놓은 사전입니다. 예를 들어 '이', '그', '저기' 등이 있습니다.
BIO 태깅
BIO 태깅이란 개체명 인식에서 사용되는 대표적인 방법 중 하나입니다. Beginning, Inside, Outside의 앞 글자를 따 BIO라고 불립니다.
B-tag: 개체명의 시작 (Beginnig)
I-tag: 개체명의 내부 (Inside)
O-tag: 개체명의 외부(Outside)
이 과정은 개체명 인식의 문맥 파악을 위해 필요합니다. 개체명 인식은 단어 뿐만 아니라 문맥이 굉장히 중요하게 작용합니다. BIO 태깅 과정을 거친다면, 모델이 문맥을 더 잘 이해할 수 있도록 도울 수 있습니다.
더불어 모델이 태깅 작업을 쉽게 학습할 수 있으며, 여러 단어로 구성된 개체명도 효과적으로 처리할 수 있다는 장점이 있습니다.
첫 번째 시도
def bio_tagging(tokens, entities):
bio_tags = ['O'] * len(tokens)
for entity, label in entities:
entity_tokens = entity.split()
for i in range(len(tokens)):
token, pos = tokens[i]
if tokens[i:i+len(entity_tokens)] == entity_tokens:
bio_tags[i] = f'B-{label}'
for j in range(1, len(entity_tokens)):
bio_tags[i + j] = f'I-{label}'
break
return bio_tags
이 과정에서 복잡한 에러가 하나 발생합니다.
ERR) not enough values to unpack (expected 2, got 1) 에러
이 에러는 entities의 각 요소가 (entity, label) 형식이 아닌 값을 포함한 것으로 예상될 때 발생합니다. 따라서 해결을 위해 입력 데이터에 검증을 추가하고, 엔터티를 글자 단위로 분리해 길이 계산까지 수행해주었습니다.
def bio_tagging(tokens, entities):
bio_tags = ['O'] * len(tokens)
# tokens 리스트의 각 요소가 (token, pos) 형식인지 확인
if not all(isinstance(token, tuple) and len(token) == 2 for token in tokens):
return bio_tags
# 토큰의 텍스트만 추출
token_texts = [token for token, pos in tokens]
for entity, label in entities:
entity_tokens = list(entity) # entity를 글자(음절) 단위로 분리
entity_length = len(bio_tags)
for i in range(len(token_texts) - entity_length + 1):
if token_texts[i:i+entity_length] == bio_tags:
bio_tags[i] = f'B-{label}'
for j in range(1, entity_length):
bio_tags[i + j] = f'I-{label}'
break
return bio_tags
하지만 BIO 태깅이 제대로 이뤄지지 않았습니다.
bio_tags
0 [O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, ...
1 [O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, ...
2 [O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, ...
3 [O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, ...
4 [O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, ...
실패를 통해 BIO 구현 방식에 대한 감을 잡았습니다.
두 번째 시도
이번에는 token_texts를 출력해 토큰이 제대로 추출되었는지 확인하고, 어절 단위로 분리해 entity_tokens를 출력해주었습니다.
def bio_tagging(tokens, entities):
bio_tags = ['O'] * len(tokens)
# tokens 리스트의 각 요소가 (token, pos) 형식인지 확인
if not all(isinstance(token, tuple) and len(token) == 2 for token in tokens):
return bio_tags
# 토큰의 텍스트만 추출
token_texts = [token for token, pos in tokens]
print(f"token_texts: {token_texts}")
for entity_label in entities:
if not isinstance(entity_label, tuple) or len(entity_label) != 2:
print(f"Skipping invalid entity_label: {entity_label}")
continue
entity, label = entity_label
entity_tokens = entity.split() # entity를 어절 단위로 분리
print(f"entity_tokens: {entity_tokens}")
entity_length = len(entity_tokens)
for i in range(len(token_texts) - entity_length + 1):
if token_texts[i:i+entity_length] == entity_tokens:
print(f"Match found: {token_texts[i:i+entity_length]} == {entity_tokens}")
bio_tags[i] = f'B-{label}'
for j in range(1, entity_length):
bio_tags[i + j] = f'I-{label}'
break
return bio_tags
이 문제를 해결하기 위해 entities 열을 리스트 형태로 변환하고 다시 BIO태깅 해주었습니다.
# 'entities' 열을 문자열에서 리스트로 변환
token_nxne_df['entities'] = token_nxne_df['entities'].apply(ast.literal_eval)
# 데이터프레임에 BIO 태그 추가
token_nxne_df['bio_tags'] = token_nxne_df.apply(lambda row: bio_tagging(eval(row['tokens']), row['entities']), axis=1)
하지만 여전히 BIO 태깅이 제대로 이뤄지지 않는 경우가 많았습니다.
디버깅
def bio_tagging(tokens, entities):
bio_tags = ['O'] * len(tokens)
# tokens 리스트의 각 요소가 (token, pos) 형식인지 확인
if not all(isinstance(token, tuple) and len(token) == 2 for token in tokens):
return bio_tags
# 토큰의 텍스트만 추출
token_texts = [token for token, pos in tokens]
print(f"token_texts: {token_texts}")
for entity_label in entities:
if not isinstance(entity_label, tuple) or len(entity_label) != 2:
continue
entity, label = entity_label
entity_tokens = entity.split() # entity를 어절 단위로 분리
print(f"entity_tokens: {entity_tokens}")
entity_length = len(entity_tokens)
for i in range(len(token_texts) - entity_length + 1):
print(f"Comparing: {token_texts[i:i+entity_length]} with {entity_tokens}")
if token_texts[i:i+entity_length] == entity_tokens:
print(f"Match found: {token_texts[i:i+entity_length]} == {entity_tokens}")
bio_tags[i] = f'B-{label}'
for j in range(1, entity_length):
bio_tags[i + j] = f'I-{label}'
break
return bio_tags
# 데이터프레임에 BIO 태그 추가
token_nxne_df['bio_tags'] = token_nxne_df.apply(lambda row: bio_tagging(eval(row['tokens']), row['entities']), axis=1)
# 결과 확인
print(token_nxne_df[['tokens', 'bio_tags']].head())
코드가 어느정도 정상동작하니, 디버깅 모드에 들어가보았습니다.
그래서 서브워드 방식을 도입했습니다. 서브워드는 단어를 여러 개의 작은 단위로 분리해 처리하는 것입니다. 그냥 단어가 예상보다 길게 태깅되고 있으니, 더 작게 분리해 문제를 해결해보자는 단순한 해결 방식입니다.
세 번째 시도
# 디버깅용 코드
def bio_tagging(tokens, entities):
# 토큰의 텍스트만 추출하여 하나의 문자열로 결합
token_texts = ''.join([token for token, pos in tokens])
print(f"token_texts: {token_texts}")
# 글자 단위로 bio_tags 리스트 생성
bio_tags = ['O'] * len(tokens)
for entity_label in entities:
if not isinstance(entity_label, tuple) or len(entity_label) != 2:
print(f"Skipping invalid entity_label: {entity_label}")
continue
entity, label = entity_label
entity_length = len(entity)
for i in range(len(tokens) - entity_length + 1):
current_tokens = token_texts[i:i + entity_length]
print(f"Comparing: {current_tokens} with {entity}")
if current_tokens == entity:
print(f"Match found: {current_tokens} == {entity}")
bio_tags[i] = f'B-{label}'
for j in range(1, entity_length):
if i + j < len(bio_tags):
bio_tags[i + j] = f'I-{label}'
break
return bio_tags
완벽하지 않지만, 제가 기대하던 수준의 BIO 태깅은 완성되어 다음 단계로 넘어갔습니다.
모델 구축
버트 인풋 만들기
이제 모델 학습을 준비할 시간입니다. 이 프로젝트에선 BERT 모델을 이용하니 해당 모델이 이해할 수 있도록 입력값을 만들어줘야 합니다. BERT의 입력은 다음과 같은 요소로 구성되어 있습니다.
1. 토큰
2. 입력 아이디
3. 세그먼트 아이디
4. 어텐션 마스크 (1이면 모델 학습, 0이면 무시)
5. 특수 토큰 (CLS면 문장 시작, SEP면 문장을 구분하거나 문장 끝)
이런 것들로 구성되어있지만, 선행 연구에서는 3개만 사용했기 때문에 저도 3개만 제작해보았습니다.
# 입력값 생성
input_ids_list = []
attention_mask_list = []
label_ids_list = []
max_len = 31 # 상위 97.5%에 해당하는 문장 길이
for i in range(len(bio_nxne_df)):
sentence = bio_nxne_df.iloc[i]['sentence']
labels = bio_nxne_df.iloc[i]['bio_tags']
# 서브워드 토큰화 및 라벨 정렬
tokenized_sentence, aligned_labels = tokenize_and_preserve_labels(sentence, labels)
# input_ids 및 attention_masks 생성
input_ids = tokenizer.convert_tokens_to_ids(tokenized_sentence)
attention_masks = [1] * len(input_ids)
# 패딩 추가
padding_length = max_len - len(input_ids)
if padding_length > 0:
input_ids = input_ids + ([tokenizer.pad_token_id] * padding_length)
attention_masks = attention_masks + ([0] * padding_length)
aligned_labels = aligned_labels + ([-100] * padding_length)
else:
input_ids = input_ids[:max_len]
attention_masks = attention_masks[:max_len]
aligned_labels = aligned_labels[:max_len]
input_ids_list.append(input_ids)
attention_mask_list.append(attention_masks)
label_ids_list.append(aligned_labels)
# 데이터프레임 생성
bert_input_nxne_df = pd.DataFrame({
'input_ids': input_ids_list,
'attention_masks': attention_mask_list,
'label_ids': label_ids_list
})
이제 인풋값도 완성되었으니 모델 구현을 통해 버트 인풋을 입력해줄 생각에 기대 만반이었습니다만.
모델마다 문제가 생기고 맙니다. 첫 번째 모델은 호환성 문제가 납니다. 이게 왜 화가 나냐면 이 친구에 맞춰 다운그레이드를 시켜주면, 다른 두 모델이 사용되지 않는 환경입니다. 그래서 그냥 전용 모델이 아닌 한국어 모델을 파인튜닝해서 사용해주려고 했습니다. 파인튜닝은 사전 학습된 모델을 특정 작업에 맞게 추가 학습 시키는 과정을 의미합니다.
저라는 쪼렙에게 높은 난이도를 자랑하는 것이지요.
데이터 분할
일단 훈련 데이터와 검증 데이터를 분할해주었습니다. 본래 더 많은 양을 학습시켜야하였으나, BIO 태깅에서 많은 시간을 소요해서 최대한 단축해 사용해주었습니다.
bert_input_sxne_df_path = '/content/drive/MyDrive/bert_input_sxne_df.csv'
bert_input_sxne_df = pd.read_csv(bert_input_sxne_df_path)
# 데이터 크기 조정
sxne_train_df, _ = train_test_split(bert_input_sxne_df, train_size=10000, random_state=42)
_, sxne_val_df = train_test_split(bert_input_sxne_df, train_size=len(bert_input_sxne_df) - 1000, random_state=42)
그리고 두근대는 마음을 부여잡고 모델 학습을 시작했습니다. 모델이 아직 정상 동작하는지 확인하고자, 간단한 샘플 텍스트를 이용해 작동시켰습니다.
from transformers import BertTokenizer, BertForSequenceClassification
import torch
# 토크나이저와 모델 로드
tokenizer = BertTokenizer.from_pretrained('KPF/KPF-bert-ner')
model = BertForSequenceClassification.from_pretrained('KPF/KPF-bert-ner')
# 모델의 임베딩 레이어 크기 확인 및 조정
model.resize_token_embeddings(len(tokenizer))
# 샘플 텍스트
text = "언론진흥재단 BERT 모델을 공개합니다."
# 텍스트를 토크나이즈하고 인코딩
encoded_input = tokenizer(text, return_tensors='pt')
# 모델 예측
with torch.no_grad():
outputs = model(**encoded_input)
# 결과 출력
print(outputs)
그런데 제가 열심히 전처리한 데이터셋에서는···.
정체불명의 에러를 띄웁니다. 에러 메시지가 뜨는 것이 아니라, epoch이 2로 넘어가지 않으며 종료되었습니다. 사실 이 전에도 다양한 시도가 있었습니다. 제가 버트 인풋에 대한 이해도가 적어 모델이 계속 받아먹지 못했기 때문이죠. 모델이 받아먹을 수 있는 버트 인풋을 만들기 위해 진땀 빼며 노력했고, 드디어 동작하기에 컴퓨터를 켜놓고 잠시 외출을 하게 되었습니다.
며칠 간 빠져나오지 못한 코딩 지옥에서 잠시 코에 바람을 쐰 저는, 제출 마감 기한이 2일 남은 시점에서 저 결과를 마주하게 됐고 맥이 풀림과 동시에 아무런 미련도 남지 않기에 포기선언을 하였습니다.
그렇게, 저의 한국어 개체명 인식(NER) 프로젝트는 실패로 막을 내리게 된 것입니다.
해당 프로젝트 진행 과정이 담긴 Colab 링크입니다.
한국어 데이터셋을 이용한 한국어 개체명 인식 모델 연구.ipynb
Colab notebook
colab.research.google.com
NER(Named Entity Recognition) 진행 방식에 대한 연구를 상당한 시간 진행했기 때문에, 해당 정리본이 어느정도는 도움이 될지도 모릅니다. 하지만 모델링 학습에 모두 실패했기에, NER의 진행 방식에만 초점을 맞춰 가볍게 훑어봐주셨으면 합니다.
나름 NER의 작동 방식을 이해했다고 생각해 갈수록 애착이 생긴 프로젝트였는데 도전한 모든 모델 학습에서 거듭 실패를 반복해 많은 아쉬움이 남습니다. 아쉬움과는 별개로 미련은 없으니 만족합니다.
마치며
사실 이 프로젝트 회고록을 작성하는 것은 프로젝트 마감으로부터 꽤 시간이 지나서입니다. 실패한 프로젝트의 회고는 부끄러움만 남을 뿐이라며 외면해왔기 때문입니다.
그런데, 시간이 지날수록 이때처럼 노력해본 순간이 없었단 게 떠오릅니다. 아무것도 모르는 상태에서 새벽마다 선행 지식 없이 이해하기 어려운 전문 서적을 펼치고, 조금이라도 비슷한 프로젝트를 찾아내어 적용해보려고 밤을 새가며 노력했던 순간들. 매일 과제 해결 방법을 고민하며 몰입했던 그 순간이, 이제 돌이켜보면 참 소중한 것 같습니다.
우습게도 이젠 어려운 과제가 눈 앞에 놓일 때마다 이 프로젝트가 떠오릅니다. 그래서 프로젝트의 회고록을 작성하게 된 것 같습니다.
프로젝트를 진행하면서 가장 크게 직면한 문제는 제가 항상 단계를 뭉뚱그려 나누었다는 점이었습니다. 처음에는 주요 기능들을 나누어 작업하려고 했지만, 그 과정에서 오류가 나는 부분을 찾지 못했습니다. 트러블슈팅을 하는 과정에서 조금씩 기능을 더 세부적으로 나누고, 단순화하는 방법을 찾을 수 있었습니다. 각 기능을 더욱 세밀하게 나누고, 단계별로 구현해 나가니 비로소 조금씩 진전이 생기고 코드가 실행되기 시작했습니다.
이 경험 덕분에, 지금은 작은 토이 프로젝트를 통해 하나하나 단계를 밟아가며 프로젝트를 완수하는 경험을 늘려가고 있습니다. 거대한 실패 앞에 자잘한 실패는 더 이상 부끄럽지 않기 때문입니다. 하하하하하ㅏ핳 이때만큼 실패하진 않을거란 자신이 있거든요!
저는 프론트엔드 개발을 목표로 전과했기 때문에, 아마 이 프로젝트가 20대의 마지막 딥러닝 프로젝트겠지만, 덕분에 그저 열정만으로 부딪혀야할 순간이 다시 찾아오면 조금 더 능숙하게 헤쳐나갈 수 있을 것 같네요. 읽어주셔서 정말 감사합니다!
'Oops, All Code! > 📝 Study Notes' 카테고리의 다른 글
[Git] 작업 되돌리기, 스테이징 되돌리기, 커밋 되돌리기, 특정 커밋으로 돌아가기 (1) | 2024.05.31 |
---|---|
[GitHub] error: cannot run notepad++: No such file or directory 혹은 힌트: 편집기가 파일을 닫기를 기다리는 중입니다... 에러 (1) | 2024.05.30 |
[GitHub] git init default 값 main으로 변경하고 git init 취소하기 (0) | 2024.05.27 |
[GitHub] 깃허브에 앱 배포하기 (0) | 2024.05.26 |
삽입정렬의 시간복잡도는 왜 이렇게 구하는거임? (0) | 2023.07.31 |
- Total
- Today
- Yesterday
- 타입좁히기
- 플리마켓후기
- 프로토타입
- 프리코스
- 코딩테스트
- 플리마켓운영
- 카드뉴스
- 프론트엔드
- 우아한테크코스
- javascript
- 카페추천
- 일급객체
- 서평
- 안성스타필드
- typescript
- 도서추천
- 어른의어휘공부
- 대학생플리마켓
- 소사벌맛집
- 소사벌
- 트러블슈팅
- 책추천
- 회고
- 도서리뷰
- 대학생팝업스토어
- react
- 경험플리마켓
- js
- 비즈플리마켓
- 어휘력
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |