プログラミング雑記 2026年10月2日
AIタグが付けられた新着記事 - Qiita
今日もネットの海から拾ってきた、個人的に気になった技術ニュースや記事を厳選してお届けします。ご意見や補足があればコメントで教えていただけると嬉しいです。 プログラミング Python Python 3.10〜3.14の各バージョンにおいてセキュリティ修正などを含...
Score: 57.4Confidence: 54%
View offerLoading the catalog…
THE AI OPPORTUNITY INDEX
Find your next AI tool. Explore free access, trials, and credits — all in one place.
AIタグが付けられた新着記事 - Qiita
今日もネットの海から拾ってきた、個人的に気になった技術ニュースや記事を厳選してお届けします。ご意見や補足があればコメントで教えていただけると嬉しいです。 プログラミング Python Python 3.10〜3.14の各バージョンにおいてセキュリティ修正などを含...
Score: 57.4Confidence: 54%
View offer掘金
写过毕业论文的朋友大概都知道一件事: 论文上某句话有引用,不代表这句话真的靠谱。 赶 Deadline 的时候尤其如此。
Score: 57.1Confidence: 54%
View offer掘金
Shopify 的人最近正式回应了「从 RN 回到原生」和「为什么不用 KMP」 这个两个问题,因为它们才在 2025 年刚总结过五年 RN 实践,那时候还是所有移动 App 都已经迁到 React
Score: 57.1Confidence: 54%
View offer掘金
前言:Agent 不是“一个模型”,而是一套系统 很多人第一次接触 Agent,会以为: 但真正跑起来你会发现,LLM 只是其中一块。 一个能完成真实任务的 Agent,至少需要六个部分协同: 感知:
Score: 57.06Confidence: 54%
View offer掘金
Company Brain 是运行在 Cloudflare Workers 上的 Slack 机器人,持续记忆团队对话并基于权限隔离的记忆容器检索答案,能在 GitHub、Linear 等工具中自动执
Score: 57.06Confidence: 54%
View offervelog
1. 토큰화란? 자연어 처리(NLP)에서는 크롤링, 문서 수집, 로그 데이터 등을 통해 얻은 텍스트 데이터인 코퍼스(corpus) 를 그대로 사용하기 어렵다. 목적에 맞게 텍스트를 전처리해야 하며, 대표적인 전처리 과정에는 다음이 있다. 토큰화(Tokenization) 정제(Cleaning) 정규화(Normalization) 이 중 토큰화란 문장이나 문서에서 의미 있는 최소 단위인 토큰(token) 으로 나누는 작업이다. 토큰은 상황에 따라 단어, 형태소, 문장, 서브워드 등이 될 수 있다. wikidocs 예를 들어 다음 문장이 있다고 가정해보자. Time is an illusion. Lunchtime double so! 구두점을 제거하고 공백을 기준으로 단어 토큰화를 하면 다음과 같이 나눌 수 있다. ["Time", "is", "an", "illusion", "Lunchtime", "double", "so"] 하지만 실제 자연어 처리에서는 단순히 공백 기준으로 문장을 나누는 것만으로 충분하지 않다. 줄임말, 구두점, 숫자, 이메일 주소, 날짜, 한국어 조사처럼 예외적으로 처리해야 하는 요소가 많기 때문이다. wikidocs 2. 단어 토큰화 토큰의 기준을 단어로 설정해 나누는 방식을 단어 토큰화(Word Tokenization) 라고 한다. 영어는 띄어쓰기 단위가 단어와 비교적 잘 일치하기 때문에, 기본적인 경우 공백을 기준으로 나누는 방식이 어느 정도 작동한다. 하지만 아포스트로피, 하이픈, 약어, 고유명사, 숫자 표현 등이 등장하면 토큰화 기준을 세밀하게 설정해야 한다. 예를 들어 아래 문장에서 Don't , Jone's 는 어떻게 분리해야 할까? Don't be fooled by the dark sounding name, Mr. Jone's Orphanage. 토큰화 도구별 결과는 다를 수 있다. 도구 Don't 처리 방식 Jone's 처리 방식 word_tokenize Do , n't Jone , 's WordPunctTokenizer Don , ' , t Jone , ' , s text_to_word_sequence don't 유지 jone's 유지 from nltk.tokenize import word_tokenize, WordPunctTokenizer from tensorflow.keras.preprocessing.text import text_to_word_sequence text = "Don't be fooled by the dark sounding name, Mr. Jone's Orphanage." print(word_tokenize(text)) print(WordPunctTokenizer().tokenize(text)) print(text_to_word_sequence(text)) 이처럼 어느 결과가 정답이라고 단정하기보다, 분석 목적과 이후 모델링 방식에 적합한 토큰화 기준을 선택하는 것 이 중요하다. wikidocs 3. 토큰화 시 고려할 점 구두점과 특수문자는 무조건 제거하지 않는다 구두점이나 특수문자는 항상 불필요한 노이즈가 아니다. 문장의 경계나 특정 의미를 표현할 수 있기 때문에, 단순 삭제 전에 데이터의 역할을 확인해야 한다. 다음과 같은 사례가 있다. 표현 의미 주의점 Ph.D. 학위 약어 마침표를 제거하면 약어 정보가 훼손될 수 있음 AT&T 기업명 특수문자가 이름의 일부임 m.p.h 속도 단위 점을 기준으로 분리하면 의미가 달라질 수 있음 $45.55 가격 $ , 소수점이 의미를 가짐 01/02/06 날짜 / 가 날짜 구분자 역할을 함 123,456,789 큰 수 , 가 숫자 단위 표현에 필요함 즉, 구두점 제거는 일괄적으로 수행하기보다 데이터의 도메인과 모델의 목적에 따라 결정해야 한다. 금융 텍스트에서는 금액, 수익률, 날짜, 종목 코드와 같은 정보가 많으므로 숫자·통화기호·소수점·쉼표의 처리 기준을 특히 주의할 필요가 있다. wikidocs 줄임말과 띄어쓰기가 포함된 표현 영어에는 아포스트로피를 통해 단어가 축약되는 경우가 많다. I'm → I am we're → we are what're → what are doesn't → does not 또한 여러 단어가 하나의 의미 단위로 사용되기도 한다. New York rock 'n' roll 따라서 토큰화 과정은 단순히 공백이나 기호를 기준으로 자르는 작업이 아니라, 문맥과 언어 규칙을 고려해 의미 단위를 보존하는 작업 이라고 볼 수 있다. wikidocs Penn Treebank 토큰화 예시 Penn Treebank 토큰화 규칙의 대표적 특징은 다음과 같다. 하이픈으로 연결된 단어는 하나의 토큰으로 유지한다. doesn't 처럼 접어(clitic)가 붙은 축약형은 분리한다. from nltk.tokenize import TreebankWordTokenizer tokenizer = TreebankWordTokenizer() text = ( "Starting a home-based restaurant may be an ideal. " "it doesn't have a food chain or restaurant of their own." ) print(tokenizer.tokenize(text)) 출력 예시는 다음과 같다. [ 'Starting', 'a', 'home-based', 'restaurant', 'may', 'be', 'an', 'ideal.', 'it', 'does', "n't", 'have', 'a', 'food', 'chain', 'or', 'restaurant', 'of', 'their', 'own', '.' ] home-based 는 하나의 토큰으로 유지되고, doesn't 는 does 와 n't 로 분리되는 것을 확인할 수 있다. wikidocs 4. 문장 토큰화 문장을 기준으로 텍스트를 나누는 작업을 문장 토큰화(Sentence Tokenization) 또는 문장 분리(Sentence Segmentation)라고 한다. 가장 단순한 방법은 마침표( . ), 물음표( ? ), 느낌표( ! )를 기준으로 나누는 것이다. 하지만 마침표는 문장의 끝뿐 아니라 약어, 이메일 주소, IP 주소, 소수점 등에도 사용된다. 예를 들어 아래 문장을 단순히 마침표 기준으로 나누면 문제가 생긴다. IP 192.168.56.31 서버에 접속해 주세요. 결과는 aaa@gmail.com으로 보내주세요. IP 주소의 점, 이메일 주소의 점, 문장 종결의 점을 모두 같은 방식으로 처리하면 잘못된 문장 분리가 발생할 수 있다. 또 Ph.D. 처럼 약어 내부에 포함된 마침표도 문장 끝으로 판단하면 안 된다. wikidocs NLTK 영어 문장 토큰화 NLTK의 sent_tokenize() 는 단순한 마침표 분할보다 더 정교하게 문장을 나눈다. from nltk.tokenize import sent_tokenize text = ( "I am actively looking for Ph.D. students. " "and you are a Ph.D student." ) print(sent_tokenize(text)) [ 'I am actively looking for Ph.D. students.', 'and you are a Ph.D student.' ] Ph.D. 내부의 마침표를 문장 종결로 오해하지 않고 처리한다. wikidocs 한국어 문장 토큰화 한국어 문장 분리에는 KSS(Korean Sentence Splitter)를 사용할 수 있다. pip install kss import kss text = ( "딥 러닝 자연어 처리가 재미있기는 합니다. " "그런데 문제는 영어보다 한국어로 할 때 너무 어렵습니다. " "이제 해보면 알걸요?" ) print(kss.split_sentences(text)) [ '딥 러닝 자연어 처리가 재미있기는 합니다.', '그런데 문제는 영어보다 한국어로 할 때 너무 어렵습니다.', '이제 해보면 알걸요?' ] 한국어 NLP에서는 한국어의 문장 종결 표현과 문장 부호 특성을 고려한 도구를 사용하는 것이 유리하다. wikidocs 5. 한국어 토큰화가 어려운 이유 한국어는 영어와 달리 단순한 띄어쓰기 기준 토큰화만으로 충분한 결과를 얻기 어렵다. 가장 큰 이유는 한국어가 교착어 이기 때문이다. 교착어는 조사, 어미, 접사 등이 어근이나 명사 뒤에 붙어 문법적 의미를 만드는 언어를 말한다. 예를 들어 다음 문장을 보자. 에디가 책을 읽었다. 띄어쓰기 기준으로 분리하면 다음과 같다. ['에디가', '책을', '읽었다'] 하지만 형태소 단위로 나누면 다음처럼 분석할 수 있다. 에디 / 가 / 책 / 을 / 읽 / 었 / 다 구분 예시 설명 자립 형태소 에디, 책 독립적으로 의미를 지니는 단위 의존 형태소 가, 을, 었, 다 다른 형태소와 결합해 문법적 기능을 하는 단위 한국어 자연어 처리에서는 명사, 동사 어간, 조사, 어미 등을 분리해야 하는 경우가 많다. 따라서 한국어에서는 어절 단위 토큰화보다 형태소 토큰화(Morpheme Tokenization) 가 중요하다. wikidocs 또한 실제 한국어 데이터에는 띄어쓰기 오류가 흔하다. 제가이렇게띄어쓰기를전혀하지않고글을썼다고하더라도 글을이해할수있습니다. 한국어는 띄어쓰기가 완벽하지 않아도 문맥으로 의미를 추론할 수 있는 경우가 많다. 하지만 모델 입장에서는 단어 경계가 불명확해질 수 있으므로, 실제 서비스 데이터에서는 띄어쓰기 교정이나 형태소 분석 품질이 NLP 성능에 영향을 줄 수 있다. wikidocs 6. 품사 태깅 품사 태깅(Part-of-Speech Tagging, POS Tagging) 은 토큰마다 문법적 역할을 부여하는 작업이다. 같은 표기를 가진 단어라도 품사에 따라 의미가 달라질 수 있다. fly 동사: 날다 명사: 파리 한국어도 마찬가지다. 못 명사: 망치로 박는 물건 부사: 어떤 행동을 할 수 없다는 의미 따라서 단어의 문맥상 의미를 이해하거나, 명사만 추출해 키워드 분석을 수행하거나, 동사·형용사 중심의 감성 분석을 수행하려면 품사 정보가 유용하다. wikidocs NLTK 영어 품사 태깅 from nltk.tokenize import word_tokenize from nltk.tag import pos_tag text = "I am actively looking for Ph.D. students." tokens = word_tokenize(text) print(pos_tag(tokens)) [ ('I', 'PRP'), ('am', 'VBP'), ('actively', 'RB'), ('looking', 'VBG'), ('for', 'IN'), ('Ph.D.', 'NNP'), ('students', 'NNS'), ('.', '.') ] 대표적인 Penn Treebank 품사 태그는 다음과 같다. 태그 의미 PRP 인칭대명사 VBP 동사 RB 부사 VBG 현재분사 IN 전치사 NNP 고유명사 NNS 복수 명사 CC 접속사 DT 관사 7. KoNLPy를 이용한 한국어 형태소 분석 한국어 NLP에서는 KoNLPy를 이용해 형태소 분석과 품사 태깅을 수행할 수 있다. KoNLPy에서 활용할 수 있는 대표적인 형태소 분석기는 다음과 같다. Okt(Open Korean Text) Mecab Komoran Hannanum Kkma 대표적으로 Okt 는 다음 메서드를 제공한다. 메서드 기능 morphs() 형태소 추출 pos() 형태소와 품사 태그 추출 nouns() 명사 추출 from konlpy.tag import Okt okt = Okt() text = "열심히 코딩한 당신, 연휴에는 여행을 가봐요" print(okt.morphs(text)) print(okt.pos(text)) print(okt.nouns(text)) 출력 예시는 다음과 같다. # 형태소 분석 ['열심히', '코딩', '한', '당신', ',', '연휴', '에는', '여행', '을', '가봐요'] # 품사 태깅 [ ('열심히', 'Adverb'), ('코딩', 'Noun'), ('한', 'Josa'), ('당신', 'Noun'), (',', 'Punctuation'), ('연휴', 'Noun'), ('에는', 'Josa'), ('여행', 'Noun'), ('을', 'Josa'), ('가봐요', 'Verb') ] # 명사 추출 ['코딩', '당신', '연휴', '여행'] 같은 문장이라도 분석기마다 결과가 달라질 수 있다. 예를 들어 Okt는 가봐요 를 하나의 동사 형태로 처리할 수 있지만, Kkma는 가보 , 아요 처럼 더 세분화할 수 있다. 따라서 형태소 분석기를 선택할 때는 다음을 기준으로 판단하는 것이 좋다. 처리 속도가 중요한가 신조어·SNS 문체 처리가 중요한가 문어체·뉴스 데이터가 중심인가 명사 추출만 필요한가 세밀한 문법 분석이 필요한가 프로젝트 환경에서 설치와 운영이 쉬운가 특히 대규모 한국어 데이터 처리에서는 처리 속도가 중요한 경우가 많아 Mecab을 고려할 수 있고, SNS·구어체 데이터 분석에서는 Okt를 실험 후보로 사용할 수 있다. wikidocs 8. 정리 토큰화는 텍스트를 모델이 처리할 수 있는 의미 단위로 나누는 NLP 전처리 과정이다. 토큰의 단위는 단어, 문장, 형태소, 서브워드 등 목적에 따라 달라질 수 있다. 구두점, 특수문자, 숫자, 날짜, 통화기호는 무조건 제거하지 않고 의미를 고려해야 한다. 영어는 공백 기반 토큰화가 어느 정도 가능하지만 축약형, 약어, 하이픈 등의 예외 처리가 필요하다. 문장 토큰화에서는 마침표가 약어·이메일·IP 주소·소수점 등에 포함될 수 있음을 고려해야 한다. 한국어는 조사와 어미가 결합하는 교착어이므로, 띄어쓰기 단위보다 형태소 단위 토큰화가 중요하다. 품사 태깅은 단어의 문법적 역할을 부여해 의미 분석, 키워드 추출, 감성 분석 등에 활용할 수 있다. 한국어 형태소 분석기마다 토큰 분리와 품사 태깅 결과가 다르므로, 실제 데이터와 목적에 맞춰 비교·선택해야 한다.
velog
코드잇 데이터분석가 부트캠프를 통해 배우고 느낀 것을 적습니다. 강의/미션/위클리페이퍼/프로젝트로 나누어 작성합니다. 📅 2026.05.21 | 🗺️ 과정: 1개월차 ▓░░░░░░ | 📖 커리큘럼: 미션 4 큰 데이터를 처음부터 끝까지 탐색하며, 그래프가 그려졌다고 분석이 맞는 것은 아니라는 것을 확인한 하루였다. 🗓️ 오늘의 일정 교시 시간 내용 1~3교시 09:00~12:00 미션 진행, 정리와 시각화 4~7교시 13:00~17:00 미션 마무리, 제출, 해설 강의 8~9교시 17:00~19:00 해설과 내 코드 비교, 자습 미션 4는 하루에 끝나서, 진행한 과정부터 해설 비교까지 오늘 한 편에 모두 적는다. 1️⃣ 오늘 진행한 일 어제 배운 EDA 흐름을 큰 규모의 건강검진 데이터에 처음부터 끝까지 적용하는 미션이었다. 데이터를 불러오고 구조와 컬럼 의미를 확인하는 연습 결측치와 이상치, 중복값을 확인하고 처리하는 연습 코드로 적힌 값을 이름표로 바꾸고, 수치형과 범주형의 기술통계를 확인하는 연습 분포, 변수 간 관계, 연령대별 차이, 성별에 따른 생활습관을 시각화하는 연습 체중과 신장으로 새 지표를 만들고, 그 지표를 기준으로 집단을 비교하는 연습 마지막으로 자유롭게 탐색하는 심화 과제 미션에는 탐색하며 알아낸 내용을 보고서로 정리하는 단계도 함께 안내되어 있었다. 2️⃣ 내가 진행한 과정 2-1. 불러오고, 의미를 먼저 확인했다 🛠️ 무엇을 했나. 한글 글꼴을 설치하고 설정하는 셀을 먼저 실행한 뒤, 드라이브를 연결해 파일을 인코딩을 지정해서 불러왔다. 행과 열의 수, 컬럼 이름, 정보 요약을 차례로 확인했다. 숫자로 저장되어 있지만 의미가 범주인 컬럼은 복사본을 만들어 범주형으로 바꾸었다. 성별, 연령대, 청력, 흡연, 음주 컬럼이 여기에 해당한다. 💭 어제 배운 "숫자라고 다 숫자가 아니다"를 바로 써 볼 수 있어서 뿌듯했다. 다만 지역 코드는 범주형 목록에서 빠뜨렸다는 것을 나중에 알았다. 2-2. 결측치를 확인하고 채웠다 🛠️ 무엇을 했나. 컬럼별 결측 개수를 센 뒤, 불리언의 평균이 비율이 된다는 힌트를 이용해 비율도 퍼센트로 확인했다. 결측이 하나라도 있는 행의 수도 따로 세었다. 채울 때는 숫자형 컬럼을 하나씩 돌며 중앙값으로, 범주형 컬럼을 하나씩 돌며 최빈값의 첫 번째 값으로 채웠다. 💭 비율을 보니 결측이 가장 많은 컬럼도 전체에서 아주 작은 부분이라는 것을 알았고 마음이 놓였다. 한편 같은 셀을 복사해서 고쳐 가며 실행하다 보니 비슷한 코드가 여러 셀에 남아서 어느 셀이 최종인지 헷갈렸다. 2-3. 이상치를 확인하고 지웠다 🛠️ 무엇을 했나. 허리둘레, 수축기혈압, 식전혈당의 히스토그램을 한 줄에 세 개로 그리고 기술통계를 확인했다. 이후 키, 체중, 허리둘레, 시력, 혈압, 혈당 컬럼마다 상식적으로 불가능한 범위를 직접 정해서 그 밖의 값이 있는지 먼저 확인했다. 그다음 IQR 경계를 계산해서 경계 밖의 행을 삭제했다. 여덟 개 컬럼을 하나의 반복문에 넣어 차례로 삭제했다. 같은 셀을 다시 돌렸더니 이상치 개수가 모두 영으로 나와서 삭제가 적용된 것을 확인했다. 💭 상식 범위를 컬럼마다 따로 정하는 일이 생각보다 어려웠다. 그리고 반복문 한 번으로 여덟 개 컬럼을 연달아 지우니 행이 눈에 띄게 줄어서, 너무 많이 지우는 것은 아닌지 불안했다. 2-4. 중복을 확인하고 이름표를 붙였다 🛠️ 무엇을 했나. 중복은 안내된 순서대로 여부 확인, 개수 확인, 중복 행 확인, 제거, 인덱스 정리, 크기 확인을 차례로 했고, 중복 개수는 영이었다. 지역 코드와 연령대 코드는 제공된 매핑표를 이용해 새 컬럼으로 바꿨다. 성별 매핑표는 직접 만들어서 같은 방식으로 이름표 컬럼을 붙였다. 💭 중복이 없다는 결과를 보고 허무했지만, 없다는 것을 확인하는 것도 분석의 일부라고 생각했다. 2-5. 기술통계를 보고 분포를 그렸다 🛠️ 무엇을 했나. 숫자형 컬럼만 골라 기술통계를 출력하고, 범주형 컬럼은 반복문으로 컬럼마다 빈도를 출력했다. 이어서 수축기혈압, 신장, 체중, 허리둘레의 히스토그램을 그리고, 신장은 성별로 색을 나누어 다시 그렸다. 색과 테두리를 바꿔 보기도 했다. 체중과 혈압의 성별 비교는 같은 방식으로 반복했고, 박스플롯과 바이올린 플롯도 연습 삼아 시도했다. 그래프마다 아래에 해석을 주석으로 적었다. 💭 그래프를 그리고 곧바로 해석 한두 줄을 적는 습관이 생겨서 만족스러웠다. 다만 바이올린 플롯 셀은 제목과 코드의 대상 변수가 달랐는데, 주석의 해석도 그대로 이어졌다. 2-6. 변수 사이의 관계를 확인했다 🛠️ 무엇을 했나. 그래프에 쓸 표본을 뽑는 셀까지 만들었지만 실제 그래프를 그리는 코드는 주석으로만 남아 있다. 상관계수는 네 개 변수를 골라 계산하고 히트맵으로 그렸다. 체중과 허리둘레의 관계가 가장 강하게 나타났다. 💭 상관 그래프를 반만 완성한 채 지나간 것이 아쉬웠다. 히트맵으로는 강도를 확인했지만 점들이 어떤 모양인지는 보지 못했다. 2-7. 연령대별 차이와 성별 생활습관을 비교했다 🛠️ 무엇을 했나. 연령대별로 평균을 구해 신장, 체중, 허리둘레, 혈압을 막대그래프로 그리고 각각 해석을 적었다. 성별에 따른 흡연과 음주는 교차표로 개수와 비율을 만들고 누적 막대그래프로 나타냈다. 흡연은 남성이 과거와 현재를 합쳐 훨씬 높았고, 음주도 남성이 더 높았다. 💭 교차표에서 비율로 바꾸니 인원수 차이에 가려져 있던 모습이 보였다. 다만 흡연은 원본 표로, 음주는 정리한 표로 그리는 식으로 서로 다른 데이터를 섞어 썼다는 점이 걸린다. 2-8. BMI를 만들고 집단을 비교했다 🛠️ 무엇을 했나. 체중을 신장 제곱으로 나누어 BMI 열을 만들고, 구간을 나누는 함수로 저체중, 정상 체중, 과체중, 비만 열을 만들었다. 그 뒤 BMI 분포, 성별 비교, 연령대별 평균과 비만도 비율, BMI와 다른 지표의 산점도와 상관 히트맵, 비만도별 흡연과 음주 비교까지 이어서 그렸다. 연령대별 해석에는 BMI가 나이가 들수록 올라간다고 적었고, 흡연은 비만일수록 비흡연이 늘어난다고 적었다. 💭 그래프가 계속 그려지니 잘 되고 있다고 믿었다. 해석 문장도 그럴듯하게 이어져서 의심하지 않았다. 2-9. 마지막 심화 과제는 비어 있다 🛠️ 무엇을 했나. 자유롭게 탐색하는 마지막 심화 과제에는 손을 대지 못했다. 💭 시간이 다 되어서 비워 둔 것이 아쉬웠다. 해설을 듣고 나서 처음부터 다시 해 보고 싶다. 3️⃣ 강의에서 배우고 써먹은 점 강의에서 배운 것 미션에서 써먹은 곳 의미가 범주인 숫자는 범주형으로 바꾼다 성별, 연령대, 청력, 흡연, 음주 컬럼 변환 수치형은 중앙값, 범주형은 최빈값으로 채운다 결측치 반복문 처리 이상치는 상식 확인 뒤에 IQR로 본다 컬럼별 비현실 범위 확인과 경계 계산 기술통계표를 질문하며 읽는다 평균과 중앙값, 최솟값과 최댓값 점검 산점도와 히트맵의 역할 구분 상관 히트맵으로 관계의 강도 확인 코드값을 이름표로 바꾸어 읽는다 지역명, 연령대, 성별 컬럼 생성 연령대별 여러 지표를 함께 읽는다 연령대별 신장, 체중, 허리둘레, 혈압 비교 바이올린 플롯으로 분포 모양을 본다 허리둘레의 성별, 연령대별 분포 💡 어제 강의에서 하나씩 배운 것이 미션에서는 한 줄로 이어졌다. 같은 흐름을 한 번 더 직접 돌려 보면서 순서가 몸에 붙는 느낌이 들었다. 4️⃣ 해설 강의에서 배운 것: 질문과 답, 그리고 느낀 점 해설은 정답을 알려 주기보다 왜 그 순서로 하는지를 짚었다. 내가 막힌 곳에 맞춰 정리한다. 4-1. 특수 코드는 결측 처리 전에 분리한다 ❓ 의문. 시력 컬럼은 다른 수치 컬럼처럼 중앙값으로 채우면 되는지 헷갈렸다. 📖 배움. 미션의 데이터 설명에는 시력의 실명을 9.9로 표기한다고 적혀 있었다. 즉 시력에는 측정값이 아닌 특수 코드가 섞여 있었다. 해설은 결측을 채우기 전에 그 코드를 먼저 별도의 표시 열로 남기고, 원래 열에서는 결측으로 바꾼 뒤에 수치형 결측과 함께 처리했다. 순서가 바뀌면 특수 코드가 평균과 중앙값에 섞여 통계가 왜곡된다. import numpy as np import pandas as pd check = pd.DataFrame({'점수': [8.5, 7.0, 99, 6.5]}) # 99는 미응시 표시라고 가정 check['미응시'] = np.where(check['점수'] == 99, 1, 0) # 1단계 표시 열 남기기 check['점수'] = check['점수'].replace(99, np.nan) # 2단계 결측으로 바꾸기 check['점수'] = check['점수'].fillna(check['점수'].median()) # 3단계 중앙값으로 채우기 print(check) 💭 데이터 설명에 분명히 적혀 있었는데도 나는 특수 코드를 따로 확인하지 않고 이상치 삭제 반복문에 그대로 맡겼다. 지워야 할 값이 아니라 의미가 있는 값이었다는 점에서 뼈아팠다. 어제 강의에서 정확히 이 부분을 경고했는데도 놓쳤다. 4-2. 이상치는 탐지 기준일 뿐이다 ❓ 의문. IQR 경계 밖의 값은 모두 지워도 되는지 궁금했다. 📖 배움. IQR은 이상치 후보를 찾는 기준이지 무조건 지우라는 뜻이 아니다. 해설은 가장 기본적인 연습에서는 간단하게 삭제했고, 실무에서는 이상치 집단을 따로 뽑아 특성을 비교하는 확장이 필요하다고 짚었다. 또 해설은 허리둘레, 수축기혈압, 식전혈당 세 컬럼만 대상으로 삼았고, 한 컬럼씩 삭제 뒤의 크기를 확인했다. 💭 나는 여덟 개 컬럼에 같은 기준을 한꺼번에 적용했다. 대상 컬럼을 고르는 것부터가 판단의 일부였다는 것을 알았다. 4-3. 순서가 있는 범주는 순서를 알려 준다 📖 배움. 연령대는 문자열로만 두면 정렬과 그래프에서 순서가 꼬일 수 있다. 순서가 있는 범주라고 지정하면 집계와 그래프가 자연스러운 순서로 나온다. ages = pd.Series(['30대', '10대', '20대']) order = ['10대', '20대', '30대'] ages = pd.Categorical(ages, categories=order, ordered=True) # 순서를 알려 준다 print(pd.Series(ages).sort_values()) 💭 내 데이터에서는 우연히 순서가 맞게 나왔지만, 문자열 정렬에 기대고 있었다는 점이 찜찜했다. 4-4. 집단 차이는 박스플롯이 설명하기 좋다 📖 배움. 성별 차이처럼 두 집단의 차이를 말할 때는 겹쳐 그린 히스토그램보다 박스플롯이 중앙값과 사분위 범위를 한눈에 비교하게 해 주어 설명력이 좋다. 히스토그램은 겹침이 심하면 읽기 어렵다. 💭 나는 히스토그램에 색만 나누어 그렸는데, 비교가 목적이라면 도구도 목적에 맞게 골라야 한다는 것을 배웠다. 4-5. 비율을 볼 때는 어느 쪽 기준인지 정한다 ❓ 의문. 교차표를 비율로 바꿀 때 행 기준과 열 기준의 차이가 무엇인지 헷갈렸다. 📖 배움. 행 기준은 각 집단 안에서 구성비를 보는 것이고, 열 기준은 각 상태 안에서 집단이 차지하는 비중을 보는 것이다. 어떤 질문을 하느냐에 따라 기준이 달라진다. ct = pd.crosstab( ['남', '남', '여', '여', '여'], ['흡연', '비흡연', '비흡연', '비흡연', '흡연'], normalize='index' # 행 기준, 열 기준이 필요하면 'columns' ) print((ct * 100).round(1)) 💭 나는 행 기준으로만 썼는데, 열 기준이라는 선택지가 있다는 것을 알고 나니 질문에 맞게 고르는 것이 중요하다고 느꼈다. 4-6. 같은 데이터프레임을 끝까지 쓴다 📖 배움. 해설은 처음부터 끝까지 하나의 표를 정리해 가며 이어서 썼다. 각 단계에서 만든 컬럼도 같은 표에 쌓았다. 새 지표를 만들 때도 같은 표 안의 컬럼끼리 계산했다. 💭 나는 원본 표와 복사본을 번갈아 쓰면서 각 그래프가 어느 표의 결과인지 구분하기 어려워졌다. 이번 미션에서 가장 크게 느낀 교훈이다. 4-7. 평균만 보면 구성의 차이가 가려진다 📖 배움. 연령대별 평균 BMI는 큰 차이가 없어 보였지만, 비만도를 구간으로 나누어 비율을 보니 중년층에서 비만이 늘고 고령층에서 다시 줄어드는 구성의 차이가 드러났다. 평균과 구성비를 함께 연결해서 설명해야 한다. 💭 내가 쓴 해석은 이 설명과 정반대였다. 그래프가 그려졌다는 이유로 맞다고 믿은 것이 문제였다. 💡 평균 하나로 말하는 것과 구성을 나누어 보는 것은 같은 데이터에서도 전혀 다른 결론으로 이어질 수 있다. 4-8. 자유 EDA는 질문에서 시작한다 📖 배움. 해설의 자유 EDA는 식전혈당이 높은 집단을 따로 뽑아 나머지와 평균과 구성을 비교하는 것부터 시작했다. 지역별 평균 지표, 연령대와 성별 조합의 변화, 흡연 상태에 따른 혈압과 혈당 비교처럼 질문을 먼저 정한 뒤 그에 맞는 그래프를 고르는 식이었다. 💭 나는 마지막 과제를 시작도 못 했는데, 해설을 보니 이상한 값을 별도의 집단으로 보는 발상은 어제 배운 이상치 처방과 이어져 있었다. 다시 도전하면 해 볼 수 있을 것 같다. 5️⃣ 해설과 내 코드의 다른 점 내 노트북과 해설을 문제별로 나란히 놓고 비교했다. 항목 내 코드 해설 컬럼 역할 정리 의미가 범주인 컬럼 일부를 범주형으로 바꿨다. 지역 코드는 빠졌다. 역할별로 컬럼 목록을 먼저 만들고 시작한다. 특수 코드 데이터 설명에 적혀 있었지만 따로 확인하지 못했다. 표시 열을 남기고 결측으로 바꾼 뒤 처리한다. 결측 처리 숫자형과 범주형을 반복문으로 채웠다. 같은 셀을 여러 번 복사했다. 역할별로 목록을 나누어 한 번에 채운다. 이상치 대상 컬럼 여덟 개를 한 반복문에서 삭제했다. 세 컬럼만 대상으로 삼고 단계마다 크기를 확인한다. 이상치 판단 상식 범위를 직접 정해 확인한 뒤 삭제했다. 탐지 기준일 뿐이고 실무에서는 집단 분석으로 확장한다고 짚는다. 중복값 안내된 순서대로 처리했다. 같은 순서로 처리한다. 이름표 컬럼 지역과 연령대, 성별을 새 컬럼으로 만들었다. 같은 방식이고 흡연, 음주, 청력까지 모두 만든다. 연령대 순서 순서를 지정하지 않았다. 순서형 범주로 지정한다. 기술통계 숫자형과 범주형을 나누어 확인했다. 같은 흐름이고, 교차표까지 이어서 본다. 분포 비교 색을 나눈 히스토그램을 주로 썼다. 박스플롯을 비교의 중심에 둔다. 변수 관계 표본 추출까지만 하고 페어플롯은 그리지 못했다. 표본을 뽑아 페어플롯을 그리고 해석한다. 상관 히트맵 코드와 식별자 컬럼까지 포함해서 큰 히트맵을 그렸다. 의미 있는 컬럼만 골라서 그린다. 사용한 표 원본 표와 정리한 표를 섞어 썼다. 하나의 표를 끝까지 이어서 쓴다. BMI 계산 체중은 원본 표에서, 신장은 정리한 표에서 가져와 계산했다. 같은 표의 두 컬럼으로 계산한다. 비만도 구간 미션 안내의 기준 대신 정상 체중의 상한을 더 낮게 잡은 구간을 썼다. 나와 같은 구간을 쓰고, 경계를 포함하는지를 주석으로 설명한다. 연령대별 BMI 해석 나이가 들수록 올라간다고 해석했다. 평균은 큰 변화가 없고 구성비에서 차이가 난다고 해석한다. 비만도별 흡연 비만일수록 비흡연이 는다고 해석했다. 비만일수록 흡연 경험이 많다고 해석한다. 자유 EDA 시작하지 못했다. 질문을 정해 집단 비교를 한다. 💡 비교해 보니 앞부분은 해설과 거의 같은 흐름이었지만, 달랐던 곳은 대부분 "결과는 나오지만 맞는지 확인하지 않은" 부분이었다. 특히 BMI를 서로 다른 표에서 가져와 계산한 것이 뒤의 해석을 모두 어긋나게 만들었다. 그래프가 그려진다는 것과 분석이 맞다는 것은 다르다는 것을, 5일차와 11일차에 이어 다시 확인했다. 📝 오늘의 정리 주제 내가 이해한 핵심 더 공부할 점 컬럼 정리 의미로 역할을 나누고 특수 코드를 먼저 분리한다 코드 종류 점검 습관 이상치 탐지 기준일 뿐이고 대상과 처방을 판단한다 이상치 집단 따로 분석 표 관리 하나의 표를 끝까지 이어서 쓴다 셀 정리와 최종본 구분 계산 검증 새 지표를 만들면 값이 맞는지 앞부분을 확인한다 결과가 이상하면 계산부터 의심 시각화 비교의 목적에 맞는 그래프를 고른다 박스플롯과 구성비 그래프 해석 평균과 구성을 함께 보고 결론을 쓴다 자유 EDA 처음부터 다시 📌 다음 일차 다음 일차: 14일차 (2026-05-22) 커리큘럼: Tableau 다음 강의자료 예고: 코드가 아닌 도구로 대시보드를 만드는 시각화를 시작한다.
velog
1. 텍스트 임베딩이란? 딥러닝 모델은 문장이나 단어 자체를 그대로 이해하지 못한다. 모델은 숫자 연산을 수행하므로, 자연어 텍스트를 숫자 형태로 변환해야 한다. 예를 들어 다음과 같은 단어가 있다고 하자. 사과, 바나나, 오렌지 컴퓨터는 사과 가 과일이고, 바나나 와 의미적으로 유사하다는 사실을 문자열 자체만으로 알 수 없다. 따라서 각 단어를 숫자로 표현하고, 모델이 학습을 통해 단어 간 관계를 파악할 수 있도록 만들어야 한다. 이처럼 단어·문장·문서를 연속적인 숫자 벡터로 표현하는 방식을 임베딩(Embedding) 이라고 한다. 텍스트 임베딩은 자연어 처리에서 필수적인 표현 방법이며, 대표적으로 다음 내용을 다룬다. 원-핫 인코딩(One-hot Encoding) 워드 임베딩(Word Embedding) 문서 벡터화(Document Vectorization) wikidocs 2. 텍스트를 숫자로 바꾸는 이유 자연어는 비정형 데이터다. 사람이 읽을 수 있는 문자열 형태이지만, 신경망은 벡터와 행렬 같은 수치 데이터를 입력으로 받아야 한다. 예를 들어 다음 문장을 감성 분석 모델에 입력한다고 가정해보자. 이 영화는 정말 재미있다. 모델은 문장을 바로 처리하는 것이 아니라, 보통 아래와 같은 흐름을 거친다. 텍스트 → 토큰화 → 정수 인코딩 또는 원-핫 인코딩 → 임베딩 벡터 → 신경망 모델 입력 → 예측 결과 토큰화는 문장을 단어·형태소·서브워드 등의 단위로 나누는 작업이고, 임베딩은 각 토큰을 숫자 벡터로 변환하는 작업이다. 예를 들어 재미있다 라는 단어는 아래처럼 임베딩 벡터로 표현될 수 있다. [0.15, -0.72, 0.33, 0.91, ...] 실제 임베딩은 수십 차원부터 수백 또는 수천 차원의 벡터로 구성될 수 있다. 각 숫자 하나를 사람이 직접 해석하기는 어렵지만, 벡터 전체의 위치와 방향을 통해 단어 사이의 의미적 관계를 계산할 수 있다. wikidocs 3. 원-핫 인코딩 원-핫 인코딩(One-hot Encoding) 은 단어를 표현하는 가장 기초적인 방법이다. 먼저 전체 단어 집합인 단어 사전(Vocabulary) 을 만든다. 인덱스 단어 0 나는 1 학생 2 입니다 3 딥러닝 4 공부한다 각 단어는 사전에서 자신에게 해당하는 인덱스 위치만 1이고, 나머지는 모두 0인 벡터로 표현된다. 나는 → [1, 0, 0, 0, 0] 학생 → [0, 1, 0, 0, 0] 입니다 → [0, 0, 1, 0, 0] 딥러닝 → [0, 0, 0, 1, 0] 공부한다 → [0, 0, 0, 0, 1] 벡터 안에서 1이 단 하나만 존재하기 때문에 원-핫 벡터라고 부른다. from tensorflow.keras.preprocessing.text import Tokenizer sentences = [ "나는 딥러닝을 공부한다", "나는 자연어 처리를 공부한다" ] tokenizer = Tokenizer() tokenizer.fit_on_texts(sentences) print(tokenizer.word_index) 출력 예시는 다음과 같다. { '나는': 1, '공부한다': 2, '딥러닝을': 3, '자연어': 4, '처리를': 5 } 원-핫 인코딩은 단어를 명확하게 구분할 수 있지만, 실제 자연어 처리에서는 여러 한계가 있다. 4. 원-핫 인코딩의 한계 희소 벡터 문제 단어 사전의 크기가 커질수록 원-핫 벡터의 차원도 커진다. 예를 들어 단어가 100,000개라면 하나의 단어를 표현하기 위해 길이가 100,000인 벡터가 필요하다. 하지만 실제 값은 대부분 0이고 단 하나의 위치만 1이다. [0, 0, 0, 0, ..., 0, 1, 0, ..., 0] 이처럼 대부분이 0으로 채워진 벡터를 희소 벡터(Sparse Vector) 라고 한다. 희소 벡터는 메모리와 연산 측면에서 비효율적이다. 단어 수가 늘수록 벡터 차원이 계속 증가하므로 대규모 텍스트 데이터에 그대로 적용하기 어렵다. 단어 간 의미 관계를 표현하지 못함 원-핫 벡터에서는 서로 다른 단어의 유사도를 표현하기 어렵다. 예를 들어 다음 단어들을 비교해보자. 고양이, 강아지, 자동차 사람은 고양이 와 강아지 가 동물이라는 점에서 의미적으로 더 가깝고, 자동차 와는 거리가 멀다고 판단한다. 그러나 원-핫 벡터 관점에서는 모든 단어가 서로 독립된 차원에 위치한다. 따라서 고양이 와 강아지 의 거리도, 고양이 와 자동차 의 거리도 동일하게 계산된다. 즉, 원-핫 인코딩은 단어의 동일성 은 구분하지만 단어의 의미적 유사성 은 담지 못한다. 5. 워드 임베딩 워드 임베딩(Word Embedding) 은 단어를 밀집된 실수 벡터(Dense Vector)로 표현하는 방법이다. 원-핫 인코딩과 달리 워드 임베딩은 단어 사전 크기보다 훨씬 작은 차원의 벡터를 사용한다. 사과 → [ 0.12, -0.08, 0.45, 0.31] 바나나 → [ 0.10, -0.05, 0.41, 0.29] 자동차 → [-0.72, 0.61, -0.33, 0.18] 위 숫자는 설명을 위한 예시다. 실제 값은 학습 과정에서 자동으로 결정된다. 사과 와 바나나 의 벡터 값이 비슷하고, 자동차 는 다른 방향에 위치한다면 모델은 과일 관련 단어들이 의미적으로 가깝다는 패턴을 학습할 수 있다. 워드 임베딩의 장점은 다음과 같다. 단어 사전 크기보다 훨씬 작은 차원으로 표현할 수 있다. 희소 벡터 문제를 완화할 수 있다. 학습 데이터의 문맥을 바탕으로 단어 간 유사성을 반영할 수 있다. 코사인 유사도와 같은 방법으로 단어 벡터 간 유사도를 계산할 수 있다. 딥러닝 모델의 입력으로 효율적으로 활용할 수 있다. wikidocs 6. 임베딩 레이어 딥러닝에서는 Embedding 레이어를 통해 단어 인덱스를 임베딩 벡터로 변환한다. 입력으로 단어 자체가 아니라 정수 인코딩된 단어 인덱스를 받는다는 점이 중요하다. import torch import torch.nn as nn vocab_size = 10000 embedding_dim = 128 embedding = nn.Embedding( num_embeddings=vocab_size, embedding_dim=embedding_dim ) input_ids = torch.tensor([ [1, 25, 103, 8], [7, 15, 0, 0] ]) embedded = embedding(input_ids) print(embedded.shape) 출력 텐서의 크기는 다음과 같다. torch.Size([2, 4, 128]) 차원 의미 2 배치 크기 4 시퀀스 길이 128 임베딩 차원 각 단어 인덱스는 길이 128의 밀집 벡터로 변환된다. 이 임베딩 벡터의 값은 학습 과정에서 손실 함수를 최소화하는 방향으로 업데이트된다. 7. Word2Vec Word2Vec은 주변 단어의 문맥을 활용해 단어 임베딩을 학습하는 대표적인 방법이다. 기본 아이디어는 특정 단어가 등장했을 때 주변에 어떤 단어들이 함께 등장하는지를 학습하면, 의미적으로 비슷한 단어가 유사한 벡터 공간에 배치될 수 있다는 것이다. 대표적인 구조는 다음 두 가지다. 모델 입력 예측 대상 CBOW(Continuous Bag of Words) 주변 단어 중심 단어 Skip-gram 중심 단어 주변 단어 예를 들어 아래 문장을 보자. 나는 딥러닝을 공부한다. 딥러닝을 이 중심 단어라면, CBOW는 주변 단어인 나는 , 공부한다 를 활용해 딥러닝을 을 예측한다. 반대로 Skip-gram은 딥러닝을 을 입력받아 주변 단어를 예측한다. Word2Vec은 비슷한 문맥에서 사용되는 단어를 가까운 벡터로 학습한다. 그래서 단어 간 유사도 검색, 추천, 문서 분류, 검색 시스템의 특징 추출 등에 활용할 수 있다. wikidocs 8. 문서 벡터화 단어뿐 아니라 문장과 문서도 벡터로 표현할 수 있다. 이를 문서 벡터화(Document Vectorization) 라고 한다. 문서 벡터화 방식은 크게 전통적인 통계 기반 방법과 신경망 기반 임베딩 방법으로 구분할 수 있다. 방법 핵심 아이디어 특징 BoW 단어의 등장 여부 또는 빈도 사용 단순하지만 단어 순서를 반영하지 못함 DTM 문서별 단어 빈도를 행렬로 표현 문서 분류·분석의 기본 표현 TF-IDF 문서 내 중요 단어에 가중치 부여 검색·키워드 분석에 자주 활용 평균 임베딩 문서 내 단어 임베딩의 평균 사용 간단하지만 어순과 문맥 손실 가능 Doc2Vec 문서 자체의 벡터를 함께 학습 문서 유사도·분류에 활용 가능 Transformer 기반 임베딩 문맥을 고려한 문장·문서 벡터 생성 의미 검색, RAG, 문서 군집화 등에 활용 예를 들어 금융 뉴스나 공시 문서를 분석한다면, 단순 키워드 빈도만 사용할 수도 있지만 문장의 맥락이 중요하다면 Transformer 기반 문장 임베딩을 활용하는 편이 유리할 수 있다. 9. 임베딩 활용 예시 임베딩은 자연어 처리의 다양한 작업에 활용된다. 감성 분석: 리뷰·뉴스·커뮤니티 글을 긍정, 부정, 중립 등으로 분류 문서 분류: 뉴스 기사, 고객 문의, 금융 공시, 계약서 등을 주제별로 분류 의미 기반 검색: 키워드가 완전히 일치하지 않아도 유사한 의미의 문서 검색 추천 시스템: 사용자 행동, 상품 설명, 콘텐츠 텍스트를 벡터화해 유사 항목 추천 챗봇과 RAG: 질문과 문서를 임베딩하여 관련 문서를 검색한 뒤 LLM에 제공 이상 탐지: 정상 문서와 의미적으로 크게 다른 텍스트를 탐지 예를 들어 “기준금리 인상 가능성”이라는 질문을 검색할 때, 임베딩 기반 검색은 문장에 같은 단어가 없더라도 “통화 긴축”, “금리 상향”, “중앙은행의 매파적 기조”와 관련된 문서를 유사하게 찾는 데 활용될 수 있다. 10. 정리 텍스트 임베딩은 단어·문장·문서를 딥러닝 모델이 처리할 수 있는 숫자 벡터로 변환하는 방법이다. 원-핫 인코딩은 단어를 구분하기 쉽지만, 차원이 커지고 희소하며 단어 간 의미 관계를 표현하지 못한다. 워드 임베딩은 단어를 저차원의 밀집 벡터로 표현해 메모리 효율성과 의미적 유사성 표현을 개선한다. 임베딩 레이어는 정수 인코딩된 토큰을 학습 가능한 벡터로 변환한다. Word2Vec의 CBOW와 Skip-gram은 주변 문맥을 이용해 단어 임베딩을 학습한다. 문서도 BoW, TF-IDF, Doc2Vec, Transformer 기반 방식 등으로 벡터화할 수 있다. 임베딩은 문서 분류, 유사도 검색, 추천 시스템, 챗봇, RAG 등 현대 NLP 시스템의 핵심 구성 요소다.
velog
1. 프로젝트 개요 이번 실습에서는 네이버 영화 리뷰 데이터(NSMC, Naver Sentiment Movie Corpus)를 활용해 리뷰가 긍정인지 부정인지 분류하는 감성 분석 모델을 구현했다. 감성 분석(Sentiment Analysis)은 문장이나 문서에 담긴 감정·평가를 분류하는 자연어 처리 작업이다. 영화 리뷰에서는 일반적으로 다음과 같이 이진 분류 문제로 설정한다. 레이블 의미 0 부정 리뷰 1 긍정 리뷰 데이터는 리뷰 내용이 담긴 document , 감성 레이블인 label , 데이터 식별자인 id 로 구성된다. 모델 학습에는 document 와 label 을 사용하고, id 는 분류에 직접적인 의미가 없으므로 제외한다. wikidocs 2. 데이터 구성 NSMC 데이터는 총 200,000개의 네이버 영화 리뷰로 구성된다. 데이터 구분 리뷰 수 훈련 데이터 150,000개 테스트 데이터 50,000개 전체 데이터 200,000개 훈련 데이터의 예시는 다음과 같은 구조를 가진다. id document label 9976970 아 더빙.. 진짜 짜증나네요 목소리 0 3819312 흠... 포스터보고 초딩영화줄... 1 10265843 너무재밓었다그래서보는것을추천한다 1 한국어 리뷰 데이터에는 띄어쓰기 오류, 이모티콘, 반복 문자, 숫자, 영어, 특수문자, 신조어 등이 포함될 수 있다. 따라서 모델 학습 전 데이터 정제와 형태소 기반 토큰화가 중요하다. wikidocs 3. 데이터 전처리 중복 데이터 제거 먼저 같은 리뷰 문장이 여러 번 존재할 수 있으므로 document 열을 기준으로 중복 데이터를 제거한다. train_data.drop_duplicates(subset=['document'], inplace=True) 원본 훈련 데이터는 150,000개였지만, 중복 제거 후에는 146,183개가 남는다. document 의 고유값 수는 146,182개이며, 여기에는 결측값 1개가 포함되어 있다. wikidocs 결측값 제거 리뷰 내용이 없는 행은 감성 분석에 사용할 수 없으므로 제거한다. train_data = train_data.dropna(how='any') 결측값을 제거한 뒤 텍스트 전처리를 수행한다. 한글 이외 문자 처리 실습에서는 한글 자음·모음·완성형 한글과 공백을 제외한 문자를 제거한다. train_data['document'] = train_data['document'].str.replace( "[^ᄀ-하-ᅵ가-힣 ]", "", regex=True ) 정규표현식의 의미는 다음과 같다. 표현 의미 ᄀ-ᄒ 한글 자음 ᅡ-ᅵ 한글 모음 가-힣 완성형 한글 음절 공백 띄어쓰기 유지 [^ ... ] 대괄호 안에 포함되지 않는 문자 특수문자 제거 이후에는 영어, 숫자, 이모티콘만으로 작성된 리뷰가 빈 문자열이 될 수 있다. 이런 데이터는 NaN 으로 바꾼 뒤 다시 제거한다. train_data['document'] = train_data['document'].str.replace( '^ +', '', regex=True ) train_data['document'].replace('', np.nan, inplace=True) train_data = train_data.dropna(how='any') 전처리 후 훈련 데이터는 145,393개, 테스트 데이터는 48,852개가 남는다. wikidocs 4. 형태소 토큰화와 불용어 제거 한국어는 조사와 어미가 단어에 결합하는 교착어이므로, 단순히 공백 기준으로 나누는 방식보다 형태소 분석 기반 토큰화가 유용하다. 실습에서는 KoNLPy의 Mecab 형태소 분석기를 사용한다. from konlpy.tag import Mecab mecab = Mecab() sentence = "와 이런 것도 영화라고 차라리 뮤직비디오를 만드는 게 나을 뻔" print(mecab.morphs(sentence)) [ '와', '이런', '것', '도', '영화', '라고', '차라리', '뮤직', '비디오', '를', '만드', '는', '게', '나을', '뻔' ] 이후 감성 분류에 상대적으로 도움이 적을 수 있는 조사, 어미, 접속 표현 등을 불용어로 정의해 제거한다. stopwords = [ '도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '지', '임', '게' ] X_train = [] for sentence in train_data['document']: tokens = mecab.morphs(sentence) tokens = [word for word in tokens if word not in stopwords] X_train.append(tokens) 예를 들어 다음 리뷰는 형태소 토큰화와 불용어 제거 후 여러 토큰으로 분리된다. 원문: 아 더빙.. 진짜 짜증나네요 목소리 토큰화 결과: ['아', '더', '빙', '진짜', '짜증', '나', '네요', '목소리'] 다만 불용어는 고정된 정답 목록이 아니다. 분석 목적, 도메인, 모델 구조에 따라 중요한 단어가 달라질 수 있으므로 실제 프로젝트에서는 데이터 탐색과 성능 평가를 바탕으로 조정해야 한다. wikidocs 5. 학습·검증·테스트 데이터 분리 모델은 훈련 데이터로 학습하고, 검증 데이터로 하이퍼파라미터와 과적합 여부를 점검하며, 마지막으로 테스트 데이터에서 최종 성능을 측정한다. 훈련 데이터의 20%를 검증 데이터로 분리한다. from sklearn.model_selection import train_test_split X_train, X_valid, y_train, y_valid = train_test_split( X_train, y_train, test_size=0.2, random_state=0, stratify=y_train ) 여기서 stratify=y_train 은 분리 이후에도 긍정·부정 레이블의 비율이 최대한 유지되도록 한다. 데이터 부정 비율 긍정 비율 학습 데이터 약 50.238% 약 49.762% 검증 데이터 약 50.239% 약 49.761% 테스트 데이터 약 49.808% 약 50.192% 레이블이 거의 균형을 이루므로 정확도(Accuracy)를 기본 평가 지표로 사용할 수 있다. 다만 실제 서비스에서 한쪽 감성이 압도적으로 많다면 정확도만으로 성능을 판단하지 말고 Precision, Recall, F1-score, ROC-AUC 등을 함께 확인해야 한다. wikidocs 6. 단어 집합과 정수 인코딩 딥러닝 모델은 문자열이 아니라 숫자를 입력으로 받는다. 따라서 토큰화된 단어에 고유한 정수 인덱스를 부여하는 정수 인코딩(Integer Encoding) 을 수행한다. 먼저 학습 데이터에서 단어의 등장 빈도를 계산한다. from collections import Counter word_list = [] for sentence in X_train: for word in sentence: word_list.append(word) word_counts = Counter(word_list) 학습 데이터에는 총 45,296개의 서로 다른 단어가 등장한다. 이 중 등장 횟수가 2회 이하인 희귀 단어는 26,105개로, 전체 단어 종류의 약 57.63%를 차지한다. 하지만 전체 등장 빈도에서 차지하는 비중은 약 2.28%에 불과하다. wikidocs 따라서 등장 빈도 3회 미만의 희귀 단어를 단어 사전에서 제외한다. threshold = 3 vocab_size = total_cnt - rare_cnt vocab = vocab[:vocab_size] 희귀 단어 제거 후 실제 단어 사전 크기는 19,191개가 된다. 특수 토큰 시퀀스 길이를 맞추기 위한 패딩과 단어 사전에 없는 단어 처리를 위해 특수 토큰을 추가한다. word_to_index = {} word_to_index['<PAD>'] = 0 word_to_index['<UNK>'] = 1 토큰 인덱스 역할 <PAD> 0 길이가 짧은 문장을 채우는 패딩 토큰 <UNK> 1 단어 사전에 없는 미지의 단어 최종 단어 사전 크기는 19,193개가 된다. for index, word in enumerate(vocab): word_to_index[word] = index + 2 토큰을 정수 시퀀스로 바꾸는 함수는 다음과 같다. def texts_to_sequences(tokenized_X_data, word_to_index): encoded_X_data = [] for sent in tokenized_X_data: index_sequences = [] for word in sent: index_sequences.append( word_to_index.get(word, word_to_index['<UNK>']) ) encoded_X_data.append(index_sequences) return encoded_X_data 예를 들어 토큰 시퀀스가 다음과 같다고 가정해보자. ['이야', '어쩜', '이렇게', '나', '지루', '할', '수'] 정수 인코딩 후에는 다음처럼 변환된다. [924, 1866, 128, 7, 80, 48, 34] 7. 패딩 리뷰마다 길이가 다르기 때문에, 배치 단위 연산을 하려면 모든 입력 시퀀스의 길이를 동일하게 맞춰야 한다. 이 과정을 패딩(Padding) 이라고 한다. 훈련 데이터에서 리뷰의 최대 길이는 74, 평균 길이는 약 12.3이다. 실습에서는 max_len = 30 으로 설정하며, 이 길이는 훈련 샘플의 약 92.5%를 포함한다. wikidocs max_len = 30 def pad_sequences(sentences, max_len): features = np.zeros((len(sentences), max_len), dtype=int) for index, sentence in enumerate(sentences): features[index, :len(sentence)] = np.array(sentence)[:max_len] return features 패딩 후 첫 번째 리뷰는 다음과 같은 형태가 된다. [ 924, 1866, 128, 7, 80, 48, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 문장 뒤쪽에 붙은 0 은 <PAD> 토큰이다. 데이터 패딩 후 크기 훈련 데이터 (116,314, 30) 검증 데이터 (29,079, 30) 테스트 데이터 (48,852, 30) 각 샘플은 길이 30의 정수 시퀀스로 변환된다. wikidocs 8. LSTM 감성 분류 모델 모델은 다음 순서로 구성된다. 정수 인코딩된 리뷰 → Embedding → LSTM → 마지막 은닉 상태 → Fully Connected Layer → 긍정/부정 분류 PyTorch 구현은 다음과 같다. import torch import torch.nn as nn class TextClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim): super(TextClassifier, self).__init__() self.embedding = nn.Embedding(vocab_size, embedding_dim) self.lstm = nn.LSTM( input_size=embedding_dim, hidden_size=hidden_dim, batch_first=True ) self.fc = nn.Linear(hidden_dim, output_dim) def forward(self, x): # x: (batch_size, sequence_length) embedded = self.embedding(x) # embedded: (batch_size, sequence_length, embedding_dim) lstm_out, (hidden, cell) = self.lstm(embedded) # hidden: (num_layers, batch_size, hidden_dim) last_hidden = hidden.squeeze(0) # logits: (batch_size, output_dim) logits = self.fc(last_hidden) return logits 입력과 출력의 텐서 형태는 다음과 같이 변한다. 단계 텐서 형태 설명 입력 (batch_size, sequence_length) 정수 인코딩된 단어 인덱스 임베딩 (batch_size, sequence_length, embedding_dim) 각 단어가 벡터로 변환됨 LSTM 마지막 은닉 상태 (batch_size, hidden_dim) 문장 전체를 요약한 벡터 출력층 (batch_size, 2) 부정·긍정 클래스별 점수 예를 들어 배치 크기 32, 문장 길이 30, 임베딩 차원 100, 은닉 상태 차원 128이라면 텐서 흐름은 다음과 같다. (32, 30) → (32, 30, 100) → (32, 128) → (32, 2) 9. 학습 설정 실습에서 사용한 주요 하이퍼파라미터는 다음과 같다. 항목 값 단어 사전 크기 19,193 최대 문장 길이 30 임베딩 차원 100 LSTM 은닉 상태 차원 128 출력 클래스 수 2 배치 크기 32 옵티마이저 Adam 학습률 0.001 손실 함수 Cross Entropy Loss 에포크 5 embedding_dim = 100 hidden_dim = 128 output_dim = 2 model = TextClassifier( vocab_size=vocab_size, embedding_dim=embedding_dim, hidden_dim=hidden_dim, output_dim=output_dim ) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam( model.parameters(), lr=0.001 ) 이진 분류 문제이지만 출력 노드를 2개로 두고 nn.CrossEntropyLoss() 를 사용한다. 모델은 각 클래스의 점수(logits)를 출력하며, 가장 높은 점수를 가진 클래스를 최종 예측으로 선택한다. wikidocs 10. 학습과 검증 학습 과정에서는 다음 순서가 반복된다. 배치 데이터를 모델에 입력한다. 예측 결과인 logits를 계산한다. 실제 레이블과 예측값의 손실을 계산한다. loss.backward() 로 역전파를 수행한다. optimizer.step() 으로 모델 파라미터를 업데이트한다. 각 에포크가 끝날 때 검증 데이터 성능을 확인한다. 검증 손실이 가장 낮은 모델을 저장한다. best_val_loss = float('inf') for epoch in range(num_epochs): model.train() for batch_X, batch_y in train_dataloader: batch_X = batch_X.to(device) batch_y = batch_y.to(device) logits = model(batch_X) loss = criterion(logits, batch_y) optimizer.zero_grad() loss.backward() optimizer.step() val_loss, val_accuracy = evaluate( model, valid_dataloader, criterion, device ) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model_checkpoint.pth') 모델 평가 시에는 model.eval() 과 torch.no_grad() 를 사용한다. model.eval() with torch.no_grad(): logits = model(batch_X) 코드 역할 model.train() 학습 모드 활성화 model.eval() 평가 모드 활성화 torch.no_grad() 평가 중 기울기 계산 비활성화 optimizer.zero_grad() 이전 배치의 기울기 초기화 loss.backward() 손실 기준 역전파 수행 optimizer.step() 모델 파라미터 업데이트 특히 model.eval() 은 드롭아웃, 배치 정규화처럼 학습·평가 동작이 다른 레이어가 포함된 모델에서 중요하다. torch.no_grad() 는 평가 시 기울기 계산을 생략해 메모리 사용량과 연산 시간을 줄여준다. wikidocs 11. 모델 성능 검증 손실이 가장 낮았던 모델을 불러와 최종 성능을 평가한다. 평가 데이터 Loss Accuracy 검증 데이터 0.3392 84.90% 테스트 데이터 0.3435 84.92% 훈련에 참여하지 않은 테스트 데이터에서도 약 84.92%의 정확도를 보였다. 검증 성능과 테스트 성능 차이가 크지 않기 때문에, 예시 결과에서는 극단적인 과적합 징후가 크지 않다고 해석할 수 있다. wikidocs 12. 새로운 리뷰 예측 학습한 모델은 새로운 영화 리뷰에 대해 긍정 또는 부정을 예측할 수 있다. index_to_tag = { 0: '부정', 1: '긍정' } def predict(text, model, word_to_index, index_to_tag): model.eval() tokens = mecab.morphs(text) tokens = [word for word in tokens if word not in stopwords] token_indices = [ word_to_index.get(token, word_to_index['<UNK>']) for token in tokens ] input_tensor = torch.tensor( [token_indices], dtype=torch.long ).to(device) with torch.no_grad(): logits = model(input_tensor) predicted_index = torch.argmax(logits, dim=1) return index_to_tag[predicted_index.item()] 예측 결과는 다음과 같다. 입력 리뷰 예측 결과 이 영화 개꿀잼 ᄏᄏᄏ 긍정 이딴게 영화냐 ᄍᄍ 부정 감독 뭐하는 놈이냐? 부정 와 개쩐다 정말 세계관 최강자들의 영화다 긍정 모델은 토큰화, 불용어 제거, 정수 인코딩, 임베딩, LSTM 연산을 거쳐 입력 문장에 포함된 감성 정보를 바탕으로 최종 클래스를 예측한다. 다만 신조어, 비꼬는 표현, 복합 감정, 문맥 의존적 표현에서는 오분류가 발생할 수 있다. wikidocs 13. 정리 NSMC는 긍정·부정 레이블을 가진 총 20만 건의 네이버 영화 리뷰 데이터셋이다. 중복값과 결측값을 제거하고, 정규표현식으로 텍스트를 정제했다. 한국어는 교착어이므로 Mecab 형태소 분석기를 이용해 토큰화했다. 희귀 단어를 제거하고 <PAD> , <UNK> 특수 토큰을 추가해 단어 사전을 구성했다. 토큰을 정수로 인코딩하고, 모든 리뷰 길이를 30으로 맞추기 위해 패딩을 적용했다. Embedding → LSTM → Fully Connected Layer 구조의 감성 분류 모델을 구현했다. 예시 모델은 검증 정확도 84.90%, 테스트 정확도 84.92%를 기록했다. 실제 성능 개선을 위해서는 양방향 LSTM, GRU, 사전학습 임베딩, Transformer 기반 언어 모델, 하이퍼파라미터 튜닝 등을 추가로 실험할 수 있다.
velog
등록해 둔 GitHub 이슈 15건을 "처리해줘" 한 줄로 던졌습니다. 워크트리 15개, tmux 워커 15개가 붙어서 각자 하나씩 맡는 구조입니다. 사람 개입은 게이트 두 번 — 태스크 분할 승인과 병합 전 최종 확인뿐이었고요. 끝나고 보니 전체 스위트 2467개가 그린이었는데, 정작 기억에 남는 건 테스트 숫자가 아니라 워커들이 이슈에 적힌 대로 구현하기를 거부한 순간들 이었습니다. 먼저 결론부터 봅시다 이슈 문면은 계약이 아니라 가설 입니다. 이슈를 쓸 때 저는 코드를 다 보고 쓰지 않았어요. "이렇게 하면 되겠지"를 적어둔 겁니다. 그런데 워커는 그 문장을 명세로 받아서 그대로 구현하려 들죠. 여기서 "문면과 실측이 어긋나면 실측을 이겨서 보고하라"는 규칙을 걸어두면, 이슈를 쓸 때 잘못 생각한 것들이 구현 단계에서 드러납니다. "그 PK, 같은 DB에서 두 번 돌리면 충돌합니다" 가장 명확했던 건 아웃박스 테이블 이슈였습니다. 이벤트를 sqlite 테이블에 쌓아두고 외부에서 드레인하는 구조인데, 저는 이슈에 이렇게 적었습니다. emission_id 를 PK로 쓰라고요. 이벤트마다 붙는 고유 ID니까 당연히 PK라고 생각했습니다. 워커가 반박해왔습니다. emission_id 는 프로세스-로컬 카운터 라서 프로세스가 새로 뜨면 다시 처음부터 매겨진다는 것. 그래서 같은 DB 파일에 두 번째 실행을 붙이면 PK 충돌이 납니다. 추론이 아니라 실제로 돌려서 확인한 결과였습니다. 이건 제가 이슈를 쓸 때 놓친 게 맞습니다. 행의 정체성을 프로세스가 아니라 저장소가 소유 해야 한다는 걸 놓친 거죠. 결론은 seq를 도입해서 PK를 저장소 쪽으로 옮기는 것이었습니다. lnpl_outbox( seq INTEGER PRIMARY KEY AUTOINCREMENT, emission_id TEXT NOT NULL, event, payload TEXT(JSON), created_at, delivered_at NULL ) emission_id 는 PK 자리에서 내려왔지만 사라지진 않았습니다. 소비자 쪽 중복 감지(dedupe)에는 여전히 필요하거든요. 그리고 삭제 대신 delivered_at 상태 마킹을 쓰는 건 그대로 뒀습니다 — at-least-once가 성립하려면 소비 기록이 남아야 하니까요. 재밌는 건 이 결정이 뒤에 붙은 태스크로 그대로 흘러갔다는 점입니다. seq가 단조 증가하는 커서라서, 그다음 SSE 구독 태스크의 Last-Event-ID 로 그대로 쓸 수 있었습니다. 이슈 문면대로 갔으면 커서가 없어서 거기서 또 막혔을 겁니다. 이슈 안에서 두 문장이 서로 싸우고 있었다 다른 태스크에서는 워커가 구현을 멈추고 질문을 올렸습니다. 이슈에 이런 두 조건이 같이 적혀 있었거든요. 새 기능에 시드 정책을 적용한다 기존 예제의 출력은 바이트 동일 해야 한다 (회귀 기준) 시드를 적용하면 값이 바뀌고, 값이 바뀌면 바이트 동일이 깨집니다. 제가 이슈를 쓰면서 두 요구를 각각 적었는데, 붙여놓으면 동시에 만족할 수 없는 조합이었던 겁니다. 코디네이터로서 판정해야 했습니다. 시드는 조건 없이 일반 적용하고, "바이트 동일"의 범위를 컴파일 표면으로 한정 해석 하기로 했습니다. 대신 조건 세 개를 붙였어요 — 의미가 바뀐다는 사실을 RFC 문면에 명시할 것, 테스트 두 건이 시드 값 자체까지 단언하도록 강화할 것. 해석으로 넘어간 부분은 문서에 남겨야 다음 사람이 안 헤맵니다. 판정 근거를 문서에 남기지 않으면 반박이 무의미하다 이 런에서 규칙처럼 굳어진 게 하나 있습니다. 이슈 문면에서 이탈할 때는 이탈 근거를 문서에 남긴다. 새로 만든 RFC가 세 편(0028/0029/0030)인데, 그중 두 편이 이런 판정 기록입니다. 안 그러면 몇 주 뒤에 이슈를 다시 읽은 사람이 "여기 emission_id PK라고 적혀 있는데 코드는 왜 seq지?"에서 멈춥니다. 워커의 반박이 아무리 정확해도, 기록이 없으면 그냥 구현이 명세를 안 지킨 걸로 보입니다. 정작 사고는 병합에서 났다 자율 처리가 매끄러웠던 것에 비해, 제 손이 닿는 병합 단계에서 같은 실수를 두 번 했습니다. 브랜치를 합치면서 README 충돌을 --ours / --theirs 로 한 방에 정리했는데, 그때마다 다른 태스크가 추가한 RFC 표 행이 같이 날아갔습니다. 두 번 다 통합 스위트의 README 최신성 테스트가 즉시 잡아냈습니다. 복원 커밋으로 해소하긴 했지만, 두 번째에는 절차를 바꿨습니다 — 카운트 충돌을 해소하기 전에 양쪽 diff에서 카운트가 아닌 변경분을 먼저 대조 하는 것으로요. 충돌 해소는 "둘 중 하나 고르기"처럼 보이지만, 표가 걸려 있으면 사실은 병합입니다. 배운 것 자율 처리 런의 품질은 워커가 얼마나 똑똑한가보다 어긋남이 올라올 채널이 있는가 에 더 달려 있었습니다. 이슈 문면을 계약으로 두면 워커는 틀린 설계를 성실하게 구현해냅니다. 가설로 두면 실측이 올라오고, 대신 코디네이터가 매번 판정을 해야 합니다. 15건 중 두 건에서 판정이 필요했으니 비용이 크진 않았어요. 그리고 사람이 개입한 구간이 제일 위험했다는 게 좀 웃깁니다. 워커 15개가 만든 코드는 리뷰와 스위트를 통과했는데, 정작 제가 손으로 한 --ours 두 번이 데이터를 날렸으니까요. 테스트가 없었으면 조용히 넘어갔을 겁니다.
velog
JavaScript_Day.7 DOM 트리를 타고 이동하기, 이벤트 객체, 타이머, 그리고 구조 분해 할당까지. 어제까지가 "태그 하나를 골라서 바꾸기"였다면, 오늘은 "태그 사이의 관계를 따라 움직이기"였다. 학습 일곱 번째 날이다. 오늘 배운 내용은 이렇다. 입력값 계산 : value 와 eval DOM 탐색 : 부모·자식·형제 노드, 텍스트 노드와 요소 노드의 차이 이벤트 객체 : type , target , currentTarget , preventDefault() 폼 요소 다루기 : name 접근, keyup , checked , disabled , label display 속성 : block, inline, inline-block data- 사용자 정의 속성 타이머 : setInterval , setTimeout , 외부 JS 파일, window.open , location classList 구조 분해 할당 1. 입력값으로 계산하기 클릭하면 정답이 나타나는 계산 문제 <h1>계산해봅시다</h1> <hr> <!-- 구분선 --> <div><span class="ans">3+4=</span><span class="btn">답</span></div> <div><span class="ans">5*20+60=</span><span class="btn">답</span></div> <div><span class="ans">20/5-4*5=</span><span class="btn">답</span></div> <script> // 같은 이름의 CSS 선택자가 여러 개 있으면 목록(배열처럼)으로 선택된다 let ans = document.querySelectorAll('.ans'); let btn = document.querySelectorAll('.btn'); btn.forEach(function (btn, i) { btn.addEventListener('click', function () { let ans2 = ans[i].textContent.replace('=', ''); // '3+4=' → '3+4' btn.textContent = eval(ans2); // 계산 결과로 교체 }); }); </script> 식 클릭 후 3+4= 7 5*20+60= 160 20/5-4*5= -16 querySelectorAll 로 가져온 목록을 forEach 로 돌면서, 각 버튼에 클릭 이벤트를 붙였다. forEach 의 두 번째 인자 i (인덱스)로 같은 순서의 식( ans[i] )을 찾는 것이 핵심이다. replace('=', '') 로 = 를 지워야 eval 이 식으로 계산할 수 있다. 추가로 정리: forEach는 NodeList에는 되고, HTMLCollection에는 안 된다 가져오는 방법 결과 타입 forEach querySelectorAll NodeList 사용 가능 getElementsByTagName , getElementsByClassName , children HTMLCollection 사용 불가 Day.5에서 getElementsByTagName 결과를 for in 으로 돌 때 오류가 났던 것도 이 HTMLCollection 때문이다. HTMLCollection은 일반 for 문으로 돌면 안전하다. 식을 입력해서 계산하기 식<input type="text" id="susik"><br> 값<input type="text" id="result"><br> <input type="button" value="계산" onclick="calcu()"> <script> function calcu() { let susik = document.querySelector('#susik'); let result = document.querySelector('#result'); result.value = eval(susik.value); console.log(typeof(result.value)); // string console.log(typeof(susik.value)); // string } </script> input 요소의 값은 .value 로 읽고 쓴다. value 는 사용자가 직접 입력한 값이며, 숫자를 입력해도 문자열 이다. input으로 계산 기능을 만들 때는 value 를 쓴다. 필기에는 result.value = eval(...) 뒤의 typeof 가 문자열이라고 되어 있다. 숫자 결과를 value 에 넣으면 문자열로 바뀌기 때문이다. 2. DOM 탐색: 노드 사이를 이동하기 부모 노드: parentNode <div class="frame"> <h1 class="head">제목</h1> <hr> <p class="p">본문 문단이다.</p> <button id="bu1">클릭</button> </div> <script> let bu1 = document.querySelector('#bu1'); bu1.addEventListener('click', function () { this.parentNode.style.backgroundColor = 'yellow'; // 버튼의 부모(div) this.parentNode.style.border = '3px solid blue'; }); </script> this 는 클릭된 버튼이고, this.parentNode 는 그 부모 노드 ( div.frame )다. border 는 두께 모양 색 순서라서 3px solid blue 는 "3px짜리 실선, 파란색 테두리"다. 탐색 속성 정리 속성 설명 parentNode 부모 노드 childNodes 모든 자식 노드 (텍스트, 주석 포함) children 자식 요소 노드만 firstChild / lastChild 첫 / 마지막 자식 노드 (텍스트, 주석 포함) firstElementChild / lastElementChild 첫 / 마지막 자식 요소 (공백 무시) nextElementSibling 다음 형제 요소 (공백 무시) previousElementSibling 이전 형제 요소 (공백 무시) 바로잡기 : 필기에는 firstChild 를 "첫번째 자손 노드", 이전 형제를 prevElementSibling 이라고 적었다. firstChild 는 바로 아래 단계인 자식 노드이고(자손은 손자까지 포함하는 말이다), 이전 형제 속성의 정확한 이름은 previousElementSibling 이다. 텍스트 노드와 요소 노드 <div class="frame"> <h1 class="head">제목</h1> <p class="p">본문 문단이다.</p> <button id="bu1">클릭</button> </div> <script> let f = document.querySelector('.frame'); console.log(f.childNodes.length); // 7 f.childNodes[1].style.color = 'red'; // <h1>이 빨간색으로 console.log(f.children.length); // 3 </script> childNodes 가 7인 이유는 태그 사이의 줄바꿈과 공백도 텍스트 노드 로 세기 때문이다. 인덱스 노드 0 텍스트 노드: <div> 여는 태그 뒤의 줄바꿈+공백 1 요소 노드: <h1> 2 텍스트 노드: </h1> 와 <p> 사이의 줄바꿈+공백 3 요소 노드: <p> 4 텍스트 노드: </p> 와 <button> 사이의 줄바꿈+공백 5 요소 노드: <button> 6 텍스트 노드: </button> 뒤의 줄바꿈+공백 children 은 요소 노드만 세므로 h1 , p , button 으로 3 이다. 그래서 childNodes[1] 처럼 인덱스를 쓸 때는 공백 때문에 번호가 어긋나기 쉽고, 요소만 쓰고 싶다면 children 이나 ~Element~ 속성을 쓰는 것이 편하다. firstChild와 firstElementChild <div id="frame"> <p>one</p> <p>two</p> </div> <script> let f1 = document.getElementById('frame'); let first = f1.firstElementChild; document.write(f1.firstChild + '<br>'); // [object Text] (공백 텍스트 노드) document.write(first); // [object HTMLParagraphElement] (<p>one</p>) </script> firstChild : 모든 노드 중 첫 번째. 텍스트와 주석도 포함한다. firstElementChild : 요소 노드 중 첫 번째. 텍스트와 공백은 무시한다. 형제 노드와 텍스트 노드 바꾸기 <div class="frame"> <!-- 처음으로 추가된 텍스트 --> <h1>javascript</h1> <div class="c"> <h2>css</h2> <p>h2 다음에 오는 문단이다.</p> </div> <!-- 마지막에 추가된 텍스트 --> </div> <script> let f1 = document.querySelector('.frame'); f1.firstChild.textContent = '처음으로 추가된 텍스트'; f1.lastChild.textContent = '마지막에 추가된 텍스트'; let c1 = document.querySelector('.c'); let h2 = document.querySelector('.c h2'); console.log(h2.textContent); // css // h2의 다음 형제 요소 = p h2.nextElementSibling.style.border = '2px solid red'; // div.c의 이전 형제 요소 = h1 c1.previousElementSibling.style.border = '2px dotted blue'; </script> 여기서 firstChild 와 lastChild 는 주석이 아니라, 주석 앞뒤의 공백 텍스트 노드 다. 그 노드의 textContent 를 바꾼 것이라 화면에 글이 나타난다. h2.nextElementSibling 은 h2 와 같은 부모 아래에서 바로 다음에 있는 요소( p )다. 3. 이벤트 객체 <p id="mouse1">마우스</p> <!-- html 인라인 이벤트에서는 매개변수 이름을 event로 쓴다 --> <button onclick="on(event)">클릭!</button> <script> function on(event) { // event: 현재 발생한 이벤트 객체 let event1 = event.type + '<br>' // 이벤트 종류 + event.target + '<br>' // 이벤트가 발생한 요소 + event.currentTarget + '<br>' // 이벤트가 연결된 요소 + event.defaultPrevented; // 기본 동작이 취소되었는가 document.querySelector('#mouse1').innerHTML = event1; } </script> 속성 설명 이 예제의 값 event.type 이벤트 종류 click event.target 이벤트가 발생한 요소 버튼 event.currentTarget 이벤트 핸들러가 연결된 요소 버튼 event.defaultPrevented 기본 동작이 취소되었는지 (취소됨: true) false 이 예제에서는 버튼에 직접 연결했기 때문에 target 과 currentTarget 이 같다. 자식 요소를 클릭했는데 핸들러가 부모에 달려 있는 경우에는 둘이 달라질 수 있다. preventDefault: 기본 동작 막기 태그에는 기본 동작 이 있다. 링크는 클릭하면 페이지가 이동하고, 체크박스는 클릭하면 체크된다. 이 기본 동작을 코드로 막을 수 있다. <!-- 링크의 기본 동작은 페이지 이동이다 --> <!-- onclick이 true를 반환하면 이동, false를 반환하면 이동 취소 --> <a href="https://example.com" onclick="return on()">이동할 거냐</a><br> <!-- 체크박스의 기본 동작은 체크되는 것이다 --> <input type="checkbox">커피<br> <input type="checkbox" onclick="no(event)">빵 <script> function on() { let n = confirm('정말 이동할건가요?'); // 확인: true, 취소: false return n; } function no(e) { e.preventDefault(); // 이벤트의 기본 동작을 강제로 취소 } </script> e.preventDefault() : 기본 동작을 취소시키는 메소드 다. e.defaultPrevented : 기본 동작이 취소되었는지 확인 하는 값이다. 함수가 아니라 속성이므로 () 를 붙이지 않는다. 이름이 비슷해서 헷갈리는데, "막아라"는 preventDefault() , "막혔나?"는 defaultPrevented 로 구분하기로 했다. 위 예제에서 "빵" 체크박스는 클릭해도 체크되지 않고, "커피"는 정상적으로 체크된다. 4. 폼 요소 다루기 폼 요소에 접근하는 3가지 방법 <form name="test"> <input type="text" name="test2" placeholder="입력해주세요"> </form> <script> // 1. document.폼이름.요소이름 let n = document.test.test2; n.style.border = '2px solid red'; // 2. document.forms['폼이름'].elements['요소이름'] let n2 = document.forms['test'].elements['test2']; n2.style.backgroundColor = 'yellow'; // 3. document.forms['폼이름']['요소이름'] let n3 = document.forms['test']['test2']; n3.style.fontSize = '50px'; </script> 세 방법 모두 같은 input 을 가리킨다. 형태가 짧은 순서대로 1번, 3번, 2번이다. keyup: 입력할 때마다 실시간으로 반영하기 <form name="test"> <input type="text" id="aa" name="test2" placeholder="입력해주세요"> </form> <div id="div1"></div> <script> let input = document.forms['test']['test2']; let n2 = document.querySelector('#div1'); // 키보드를 입력했을 때의 이벤트 (고전 이벤트 방식) input.onkeyup = function () { let n3 = this.value; // input에 입력한 값 n2.textContent = n3; // div 영역에 띄운다 }; </script> 키를 눌렀다 뗄 때마다 input 의 값이 div 에 그대로 나타난다. 필기에서는 innerHTML 로 넣었는데, 사용자 입력을 그대로 넣는 곳이라서 Day.6에서 정리한 이유로 textContent 로 바꿨다. disabled, label, checked 주민번호 뒷자리 첫 숫자를 입력하면 성별 라디오 버튼이 자동으로 선택되는 실습이다. <form name="test" class="a1"> <input type="text" name="input_1" value="001111" disabled> <!-- disabled: 입력하지 못하도록 막는다 --> <input type="text" name="input_2" placeholder="주민번호 뒷자리 입력하기"> <input type="radio" name="ch" id="male"> <label for="male">남</label> <!-- label로 구분 --> <input type="radio" name="ch" id="female"> <label for="female">여</label> </form> <script> let n = document.test.input_2; let male = document.querySelector('#male'); let female = document.querySelector('#female'); n.onkeyup = function () { let num = this.value; if (num >= 1 && num <= 4) { if (num == 1 || num == 3) { female.checked = false; male.checked = true; // 체크한다 } else { male.checked = false; female.checked = true; } } }; </script> disabled : 입력을 막는 속성이다. <label for="id"> : for 에 input 의 id 를 넣으면 글자를 눌러도 해당 입력이 선택된다. radio 나 checkbox 타입에는 checked 라는 상태 필드가 있어서, true / false 로 체크 상태를 바꾼다. 추가로 정리: 라디오 버튼은 한쪽만 true로 해도 된다 name 이 같은 라디오 버튼은 하나만 선택되므로, male.checked = true 만 해도 female 은 자동으로 해제된다. 위 코드의 female.checked = false 는 없어도 결과가 같다. 5. display 속성: 블록, 인라인, 인라인 블록 구분 블록 박스 인라인 박스 인라인 블록 박스 속성 display: block display: inline display: inline-block 예시 태그 div , p , h1~h6 label , a , span , strong (속성으로 지정) 새 라인에서 시작 항상 새 라인 못 함 (라인 안에 있음) 못 함 (인라인 특성) 옆에 다른 요소 배치 불가능 가능 가능 (인라인 특성) width, height 조절 가능 불가능 가능 (블록 특성) padding, margin 조절 가능 위아래 margin은 적용 안 됨 가능 (블록 특성) 필기에는 인라인 박스의 예로 textarea 와 input 도 적었는데, 이 둘은 인라인처럼 줄 안에 놓이지만 width와 height를 조절할 수 있다. 그래서 기본값이 사실상 인라인 블록에 가깝다. 위 표의 인라인 예시는 label , a , span , strong 만 남겼다. <style> div { border: 1px solid salmon; color: blue; background-color: yellow; } </style> css <div style="display: none;">재밌음</div> <br> css <div style="display: inline; height: 50px;">재밌음</div> css <div style="display: inline-block; height: 50px;">재밌음</div> css <div style="display: block;">재밌음</div> 같은 div 에 속성만 바꿔서 확인했다. none : 화면에서 사라지고 자리도 차지하지 않는다. inline : 글자 크기만큼만 차지한다. height: 50px 을 줘도 적용되지 않는다. inline-block : 옆에 붙으면서 height: 50px 이 적용된다. block : 새 줄에서 시작해 한 줄을 다 차지한다. 6. 폼 제출 이벤트로 목록 만들기 <h3>정보입력</h3> <form id="info"> <p>이름 <input type="text" id="name"></p> <p>주소 <input type="text" id="addr"></p> <input type="submit" value="정보입력"> </form> <p id="output"></p> <!-- 완성되면 이런 모양: <p>홍길동 , 서울</p> --> <script> let form = document.getElementById('info'); let nameInput = document.getElementById('name'); let addr = document.getElementById('addr'); let output = document.getElementById('output'); // submit 버튼을 눌렀을 때 발생하는 이벤트 → onsubmit form.onsubmit = function (e) { e.preventDefault(); // 기본 제출 동작(새
velog
노트북: 09-01. tokenization.ipynb (셀 18개, 코드 셀 12개) 예상 시간: 약 12분 · 짝꿍 글: 4.3_토큰화.md 개념 1분 — 이것만 알고 코드 보기 토큰화(tokenization) = 문장을 모델이 다룰 단위(토큰) 로 자르는 것. 4.1~4.2의 RNN에 "단어 하나 = 시점 하나"로 넣으려면, 먼저 "단어"가 뭔지 정해야 한다. 자르는 단위에 따라 이름이 붙는다. 단어 토큰화 : 단어 단위 문장 토큰화 : 문장 단위 형태소 토큰화 : 뜻을 가진 가장 작은 조각 단위 영어 는 띄어쓰기만으로 꽤 잘 잘린다. 문제는 Don't , Mr. , Ph.D. 같은 아포스트로피와 마침표다. 한국어 는 띄어쓰기로 자르면 안 된다. "영화가", "영화를", "영화는"이 전부 다른 단어가 되기 때문이다. 그래서 형태소 분석기 로 "영화 / 가"처럼 조사를 떼어 낸다. 4.5에서 쓰는 mecab.morphs() 가 바로 이것이다. 정답 토큰화는 없다. 도구마다 자르는 규칙이 다르고, 어떤 걸 쓸지는 우리가 고른다. 이 노트북은 그 차이를 눈으로 비교하는 노트북이다. 발표 전 체크 ⚠️ 셀 0을 그대로 실행하면 셀 3에서 멈출 수 있다. 최신 nltk(3.9 이상)는 word_tokenize 에 punkt 가 아니라 punkt_tab 이 필요하다. 그래서 LookupError: Resource 'punkt_tab' not found 가 난다. pos_tag 도 averaged_perceptron_tagger_eng 를 요구한다. 셀 0에 두 줄을 미리 추가해 두자. nltk.download('punkt_tab') nltk.download('averaged_perceptron_tagger_eng') nltk 3.10에서 직접 확인했다. 두 줄을 넣으면 나머지 출력은 노트북에 저장된 것과 완전히 같다. 셀 2는 tensorflow.keras.preprocessing.text 를 임포트한다. 코랩에는 텐서플로가 깔려 있고, Keras 3에도 이 함수가 레거시로 남아 있어서 그대로 돈다. 셀 11·15의 pip install kss , pip install konlpy 는 앞에 ! 가 없지만 코랩에서는 돌아간다. kss는 설치가 1분 넘게 걸리니 세션 전에 미리 실행 해 두자. 흐름 한눈에 파트 셀 한 줄 메시지 시간 1. 영어 단어 토큰화 3종 비교 0~5 같은 문장도 도구마다 다르게 자른다 3분 2. 트리뱅크 규칙 7 하이픈은 붙이고, 줄임말은 뗀다 1분 3. 문장 토큰화 9~12 마침표 = 문장 끝이 아니다 2분 4. 품사 태깅 14 토큰에 품사를 붙인다 1분 5. 한국어 형태소 분석 15~17 한국어는 형태소로 자른다 → 4.5로 연결 4분 읽는 법: 🗣 = 그대로 말해도 되는 문장, ❓ = 나올 만한 질문과 답 Part 1. 영어 단어 토큰화 3종 비교 (셀 0~5) 셀 0 — nltk 데이터 내려받기 import nltk nltk.download('punkt') # 문장/단어 토큰화 규칙 nltk.download('averaged_perceptron_tagger') # 품사 태깅 모델 # (추가) nltk.download('punkt_tab'); nltk.download('averaged_perceptron_tagger_eng') nltk는 코드와 데이터(규칙, 모델)가 따로 있다. 쓰기 전에 데이터를 내려받아야 한다. 셀 2~5 — 같은 문장, 세 도구 from nltk.tokenize import word_tokenize from nltk.tokenize import WordPunctTokenizer from tensorflow.keras.preprocessing.text import text_to_word_sequence s = "Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop." word_tokenize(s) WordPunctTokenizer().tokenize(s) text_to_word_sequence(s) 결과에서 갈리는 부분만 모으면 이렇다. 원문 word_tokenize WordPunctTokenizer text_to_word_sequence Don't Do , n't Don , ' , t don't Mr. Mr. Mr , . mr Jone's Jone , 's Jone , ' , s jone's , . 남김 남김 지움 대소문자 그대로 그대로 전부 소문자 토큰 수 25 28 21 word_tokenize → 영어 문법을 안다. n't (not), 's (소유격)처럼 뜻이 있는 단위 로 뗀다. WordPunctTokenizer → 규칙이 단순하다. 구두점을 무조건 따로 뗀다. 그래서 Don , ' , t 처럼 뜻이 없는 조각이 생긴다. text_to_word_sequence → 소문자로 바꾸고 구두점을 지운 뒤 띄어쓰기로만 자른다. 가장 거칠지만 가장 단순하다. 🗣 "같은 문장인데 토큰 수가 25, 28, 21로 다 달라요. 토큰화에는 정답이 없고, 어떤 단위가 우리 문제에 맞는지 고르는 거예요." ❓ 뭘 써야 해? 문제에 따라 다르다. 감성 분류처럼 "좋다/싫다"만 보면 거친 토큰화로도 충분하다. 번역처럼 문법이 중요하면 n't 같은 단위를 살리는 게 낫다. Part 2. 트리뱅크 토크나이저 (셀 7) from nltk.tokenize import TreebankWordTokenizer tokenizer = TreebankWordTokenizer() text = "Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own." tokenizer.tokenize(text) ['Starting', 'a', 'home-based', 'restaurant', 'may', 'be', 'an', 'ideal.', 'it', 'does', "n't", ..., 'own', '.'] 하이픈은 붙여 둔다 → home-based 가 한 토큰이다. 줄임말은 뗀다 → doesn't → does , n't . 눈여겨볼 곳: ideal. 의 마침표가 안 떨어졌다. 트리뱅크는 문자열 맨 끝의 마침표만 뗀다. 문장 중간 마침표는 단어에 붙은 채로 남는다. 그래서 보통은 문장 토큰화를 먼저 하고 단어 토큰화를 한다( word_tokenize 가 안에서 그렇게 한다). 🗣 "ideal 뒤 마침표가 안 떨어진 게 보이시죠. 트리뱅크는 문장 하나를 넣는다고 가정해서 맨 끝 마침표만 떼요. 그래서 문장부터 나누고 단어를 자르는 순서가 필요합니다." Part 3. 문장 토큰화 (셀 9~12) 셀 9~10 — 영어 from nltk.tokenize import sent_tokenize sent_tokenize("His barber kept his word. But keeping such a huge secret ...") # 5문장으로 잘 나뉨 sent_tokenize("I am actively looking for Ph.D. students. and you are a Ph.D student.") # → ['I am actively looking for Ph.D. students.', 'and you are a Ph.D student.'] 마침표를 기준으로 자르지만, Ph.D. 의 마침표에서는 자르지 않았다. 약어 목록을 학습해 둔 덕분이다. 두 번째 문장은 소문자 and 로 시작하는데도 잘 나눴다. 셀 11~12 — 한국어 (kss) import kss kss.split_sentences('딥 러닝 자연어 처리가 재미있기는 합니다. 그런데 문제는 영어보다 한국어로 할 때 너무 어렵습니다. 이제 해보면 알걸요?') # → ['딥 러닝 자연어 처리가 재미있기는 합니다.', '그런데 문제는 ...', '이제 해보면 알걸요?'] 한국어 문장 분리기다. 함께 찍힌 WARNING ... mecab ... 은 에러가 아니다. "환경에 mecab이 있어서 그걸 백엔드로 쓴다"는 안내다. 🗣 "마침표가 곧 문장 끝은 아니에요. Ph.D.처럼 약어에도 마침표가 들어가니까, 문장 토큰화도 규칙이나 학습이 필요합니다." Part 4. 품사 태깅 (셀 14) from nltk.tag import pos_tag tokenized_sentence = word_tokenize("I am actively looking for Ph.D. students. and you are a Ph.D. student.") pos_tag(tokenized_sentence) [('I', 'PRP'), ('am', 'VBP'), ('actively', 'RB'), ('looking', 'VBG'), ('for', 'IN'), ('Ph.D.', 'NNP'), ('students', 'NNS'), ('.', '.'), ...] 태그 뜻 예 PRP 인칭 대명사 I, you VBP / VBG 동사 현재형 / 동명사·현재분사 am / looking RB 부사 actively IN 전치사 for NNP / NNS / NN 고유명사 / 복수명사 / 단수명사 Ph.D. / students / student DT 한정사 a 토큰화한 결과에 품사 꼬리표 를 붙인다. 4.1의 입출력 구조로 보면 다 대 다 다(단어마다 출력 하나). Part 5. 한국어 형태소 분석 — KoNLPy (셀 15~17) from konlpy.tag import Okt, Kkma okt, kkma = Okt(), Kkma() s = "열심히 코딩한 당신, 연휴에는 여행을 가봐요" okt.morphs(s); okt.pos(s); okt.nouns(s) kkma.morphs(s); kkma.pos(s); kkma.nouns(s) Okt 꼬꼬마(Kkma) morphs 열심히 / 코딩 / 한 / 당신 / , / 연휴 / 에는 / 여행 / 을 / 가봐요 열심히 / 코딩 / 하 / ᄂ / 당신 / , / 연휴 / 에 / 는 / 여행 / 을 / 가보 / 아요 토큰 수 10 13 nouns 코딩, 당신, 연휴, 여행 코딩, 당신, 연휴, 여행 morphs → 형태소 단위로 자른 리스트. 4.5에서 쓰는 게 이것 이다(분석기는 Mecab). pos → 형태소 + 품사 튜플. nouns → 명사만 뽑는다. 꼬꼬마가 더 잘게 자른다. 코딩한 → 코딩 / 하 / ᄂ , 가봐요 → 가보 / 아요 . 눈여겨볼 곳: Okt는 한 을 조사(Josa) 로 태깅했다. 실제로는 하다 의 활용형( 하 + ᄂ )이다. 분석기마다 틀리는 곳이 다르다. 🗣 "한국어는 '연휴에는'을 그대로 두면 '연휴', '연휴에', '연휴를'이 전부 다른 단어가 돼요. 그래서 형태소로 떼서 '연휴'를 같은 단어로 만드는 겁니다. 4.5에서 mecab.morphs 한 줄이 이 역할을 해요." ❓ 4.5에서는 왜 Mecab을 써? 빠르기 때문이다. 4.5는 리뷰 19만 개를 토큰화한다. Mecab은 노트북 기준 14만 5천 개를 10초 정도에 끝낸다. 메서드 이름( morphs )은 Okt, 꼬꼬마와 같다. 마무리 (1분) 🗣 "토큰화는 문장을 RNN에 넣을 단위로 자르는 거고, 정답은 없고 도구마다 규칙이 달라요. 영어는 아포스트로피와 마침표 처리가 갈리고, 한국어는 조사 때문에 형태소 분석기가 필요합니다. 4.5에서는 Mecab으로 형태소를 자르고, 조사 같은 불용어를 빼고 씁니다." 치트시트 도구 언어 단위 특징 word_tokenize 영어 단어 n't , 's 분리. 가장 무난 WordPunctTokenizer 영어 단어 구두점 전부 분리 text_to_word_sequence 영어 단어 소문자화 + 구두점 삭제 TreebankWordTokenizer 영어 단어 하이픈 유지, 끝 마침표만 분리 sent_tokenize 영어 문장 약어 마침표 처리 kss.split_sentences 한국어 문장 Okt / Kkma / Mecab .morphs() 한국어 형태소 4.5는 Mecab
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%