📌 목차

1. word sense
2. one-hot encoding

3. thesaurus(어휘분류사전)
4. Feature
5. Feature Extraction. &. Text Mining (TF-IDF)
6. Feature vector 생성
7. Vector Similarity (with Norm)
8. 단어 중의성 해소 (WSD)
9. Selection Preference

😚 글을 마치며...

 

 

 


1. Word Sense

1.1 단어와 의미의 관계성
NLP에서 가장 기초가 되는 근간이자 가장 어려운 문제인 단어의 의미(word sense)와 의미의 유사성, 모호성에 대해 알아보자.
단어는 겉으로 보이는 형태인 표제어(lemma)안에 여러 의미를 담고있어서 상황에 따라 다른 의미로 사용된다.
이때, 주변정보(context)에 따라 숨겨진 의미를 파악하고 이해해야하지만
context가 부족하면 ambiguity가 증가할 수 있다. ( ∵ 과거기억 등의 이유)

즉, 한가지 형태의 단어에 여러 의미가 포함되어 생기는 '중의성' 문제는 NLP에서 매우 중요한 위치를 갖는데, 특히 기계번역에서는 단어의 의미에 따라 해당 번역 단어의 형태가 완전히 바뀌기에 매우 중요하다.
즉, lemma(표제어)를 매개체 삼아 내부 latent space의 'word sense'로 변환해 사용해야한다.

 

1.2
동형어∙다의어

 동형어: 형태는 같으나 뜻이 서로 다른 단어 (ex. 차 - tea / car)
 다의어: 동형어개념 + 그 의미들이 서로 관련이 있는 뜻 (ex. 다리 - leg / desk leg)

이때, 한 형태내 여러 의미를 갖는 동형어∙다의어의 경우, 단어 중의성해소(WSD)라는 방법을 통해 단어의 의미를 더 명확히 하는 과정이 필요하다.
단어의 의미를 더 명확히 하기 위해 주변문맥을 통해 원래단어의미를 파악해야하는데, 이때 end-to-end 방법이 DNN에서 선호된다.
이로인해 단어 중의성 해소에 대한 필요도가 낮아졌지만 아직 ambiguity는 문제해결이 어려운 경우가 많다.
동의어
 동의어: 다른 형태의 단어간에 의미가 같은 단어 (ex. home, place)

물론, 의미가 완전히 딱 떨어지지는 않고 똑같지 않을 수도 있지만 일종의 동의(consensus)가 존재하며, 이 동의어가 여러개 있을 때, 이를 동의어 집합(synset)이라 한다.
상위어∙하위어
단어는 하나의 추상적 개념을 가지며 이때, 그 개념들을 포함하는 상∙하위 개념이 존재하며 이에 해당하는 단어들을 상위어(hypemym), 하위어(hyponym)이라 한다. (ex. 동물-포유류, 포유류-코끼리)

이런 단어들의 어휘분류(taxonomy)에 따라 단어간 관계구조를 계층화 할 수 있다.

 

1.3 모호성 해소 (WSD)
컴퓨터는 오직 text만 가지므로 text가 내포한 진짜 의미를 파악하는 과정이 필요하다.
즉, 단어의 겉 형태인 text만으로는 모호성(ambiguity)이 높기에 모호성을 제거하는 과정인, WSD(단어 중의성 해소)으로 NLP의 성능을 높일 수 있다.

 

 

 

 

 


2. One-Hot Encoding

2.1 One-Hot Encoding
단어를 컴퓨터가 인지할 수 있는 수치로 바꾸는 가장 간단한 방법은 벡터로 표현하는 것으로 가장 기본적인 방법 중 하나는 one-hot encoding이라는 방식이다.
말 그대로 단 하나의 1과 나머지 수많은 0들로 표현된 encoding방식으로 one-hot encoding vector의 차원은 보통 전체 vocabulary의 개수가 된다. (보통 그 숫자는 매우 큰 수가 됨; 보통 30,000~100,000)

단어는 연속적인 심볼로써 이산확률변수로 나타낸다.
위와 같이 사전(dictionary)내의 각 단어를 one-hot encoding 방식을 통해 vector로 나타낼 수 있는데, 이 표현방식은 여러 문제점이 존재한다.
 Prob 1. vector space가 너무 커졌다. (하나만 1, 나머지는 0;  이때 0으로 채워진 vector를 sparse vector라 한다.)
 Prob 2. sparse vector의 가장 큰 문제점은 vector간 연산 시 결과값이 0이 되는, orthogonal하는 경우가 많아진다.
  - 즉, 다시 말하면 '강아지', '개'라는 단어는 상호유사하지만 이 둘의 연산 시 둘의 유사도가 0이 되어버릴 것이고, 일반화에 어려움을 겪을 수 있다.


  - 이는 Curse of Dimensionality와 연관되는데, 차원이 높아질수록 정보를 표현하는 각 점(vector)가 매우 낮은 밀도로 sparse하게 퍼져있게 된다. 따라서 차원의 저주로부터 벗어나고자 차원의 축소하여 단어를 표현할 필요성이 대두된다.

 

 

 

 

 

 

 


3. Thesaurus (어휘분류사전)

3.1 WordNet
Thesaurus(어휘분류사전)는 계층적 구조를 갖는 단어의미를 잘 분석∙분류해 구축된 데이터베이스로 WordNet은 가장 대표적인 시소러스의 일종이다.


WordNet은 동의어집합(Synset), 상위어, 하위어 정보가 특히 유향비순환그래프(DAG)로 잘 구축되어있다는 장점이 있다.
(트리구조가 아님: 하나의 노드가 여러 상위노드를 가질 수 있기 때문.)

WordNet은 워드넷 웹사이트에서 바로 이용할 수도 있다. (http://wordnetweb.princeton.edu/perl/webwn)
아래 사진을 보면 명사일 때 의미 10개, 동사일 때 의미 8개를 정의하였으며 명사 bank#2의 경우, 여러 다른표현(depository financial institution#1, banking concern#1)들도 같이 게시되어있다. (이것들이 바로 동의어 집합)
이처럼 wordnet은 단어별 여러 가능한 의미를 미리 정의하고 번호를 매기고, 동의어를 링크해 동의어 집합을 제공한다.
이는 WSD에 매우 좋은 label data가 되며 wordnet이 제공하는 이 data들을 바탕으로 supervised learning을 진행하면 단어중의성해소(WSD)문제를 풀 수 있다.

cf) 한국어 wordnet도 존재
 ∙ KorLex(http://korlex.pusan.ac.kr/)
 ∙ KWN(http://wordnet.kaist.ac.kr/)

 

3.2 WordNet을 활용한 단어간 유사도 비교
추가적으로 NLTK에 wrapping되어 포함되므로 import하여 사용가능하다.
from nltk.corpus import wordnet as wn

def hypernyms(word):
    current_node = wn.synsets(word)[0]
    yield current_node
    
    while True:
        try:
            current_node = current_node.hypernyms()[0]
            yield current_node
        except IndexError:
            break​

 

위의 코드는 wordnet에서 특정 단어의 최상위 부모노드까지의 경로를 구할 수 있고,
추가적으로 단어마다 나오는 숫자로 각 노드 간 거리를 알 수 있다. 
예를 들어, 위의 경우, 'student'와 'fireman' 사이의 거리는 5 임을 알 수 있다.

이를 통해 각 최하단 노드간의 최단거리를 알 수 있고, 이를 유사도로 치환해 활용할 수 있다.
이를 이용해 공식을 적용해보면 아래와 같다.

한계점: 사전을 구축하는데 비용과 시간이 많이 소요되고 상하위어가 잘 반영된 사전이어야만 해서 
사전에 기반한 유사도 구하기 방식은 정확성은 높으나 한계가 뚜렷하다는 단점이 존재.

 



 


4. Feature

4.1 Feature
지금까지는 one-hot vector를 통해 단어를 표현했을 때, 많은 문제가 발생한다고 설명하였다.
이는 one-hot vector의 표현방식이 효과적이지 않기 때문이다.

효과적인 정보 추출 및 학습을 위해서는 대상의 특징(feature)을 잘 표현해야한다.
이런 특징은 수치로 표현되며, 최대한 많은 samples를 설명할 수 있어야하기에 samples는 수치가 서로 다른 공통된 특징을 갖고 다양하게 표현되는것이 좋다.
즉, 각 sample의 feature마다 수치를 갖게하여 이를 모아 벡터로 표현한 것feature vector라 한다.
4.2 단어의 feature vector 구하기 위한 가정
① 의미가 비슷한 단어 → 쓰임새가 비슷할 것
② 쓰임새가 비슷 → 비슷한 문장안에서 비슷한 역할로 사용될 것
③ ∴ 함께 나타나는 단어들이 유사할 것

 

 

 


5. Feature Extraction. &. Text Mining (TF-IDF)

특징벡터를 만들기 앞서 text mining에서 중요하게 사용되는 TF-IDF를 사용해 특징을 추출하는 방법을 알아보자.

 

5.1 TF-IDF (Term Frequency-Inverse Document Frequency)

TF-IDF: 출현빈도를 사용어떤단어 w가 문서 d 내에서 얼마나 중요한지 나타내는 수치이다.
즉, TF-IDF의 값이 높을수록 w는 d를 대표하는 성질을 가진다.
TF: 단어의 문서내에 출현한 횟수
IDF: 그 단어가 출현한 문서의 숫자(DF)의 역수

예를 들어, 'the'의 경우, TF값이 매우 클 것이다.
하지만, 'the'가 중요한 경우는 거의 없을 것이기에 이를 위해 IDF가 필요하다.
(IDF를 사용함으로써 'the'와 같은 단어들에 penalty를 준다.)

∴ 최종적으로 얻게되는 숫자는 "다른 문서들에서는 잘 나타나지 않지만 특정 문서에서만 잘 나타나는 경우 큰 값을 가지며, 특정 문서에서 얼마나 중요한 역할을 차지하는지 나타내는 수치가 될 수 있다."

 

5.2 TF-IDF 구현예제
 ∙ 3개의 논문스크립트가 담긴 문서들이 doc1, doc2, doc3 변수에 들어있다 가정하자.

📌 TF 함수
def get_term_frequency(document, word_dict=None):
    if word_dict is None:
        word_dict = {}
    words = document.split()

    for w in words:
        word_dict[w] = 1 + (0 if word_dict.get(w) is None else word_dict[w])

    return pd.Series(word_dict).sort_values(ascending=False)​



📌 DF 함수
def get_document_frequency(documents):
    dicts = []
    vocab = set([])
    df = {}

    for d in documents:
        tf = get_term_frequency(d)
        dicts += [tf]
        vocab = vocab | set(tf.keys())
    
    for v in list(vocab):
        df[v] = 0
        for dict_d in dicts:
            if dict_d.get(v) is not None:
                df[v] += 1

    return pd.Series(df).sort_values(ascending=False)​



📌 TF-IDF 함수
def get_tfidf(docs):
    vocab = {}
    tfs = []
    for d in docs:
        vocab = get_term_frequency(d, vocab)
        tfs += [get_term_frequency(d)]
    df = get_document_frequency(docs)

    from operator import itemgetter
    import numpy as np

    stats = []
    for word, freq in vocab.items():
        tfidfs = []
        for idx in range(len(docs)):
            if tfs[idx].get(word) is not None:
                tfidfs += [tfs[idx][word] * np.log(len(docs) / df[word])]
            else:
                tfidfs += [0]

        stats.append((word, freq, *tfidfs, max(tfidfs)))

    return pd.DataFrame(stats, columns=('word',
                                        'frequency',
                                        'doc1',
                                        'doc2',
                                        'doc3',
                                        'max')).sort_values('max', ascending=False)

get_tfidf([doc1, doc2, doc3])​

<출력>

 

 

 

 

 


6. Feature Vector 생성

6.1 TF 행렬 만들기
TF 또한 좋은 특징이 되는데, 출현한 횟수가 차원별로 구성되면 하나의 특징벡터를 이룰 수 있다.
(물론 문서별 TF-IDF자체를 사용하는것도 좋음.)

def get_tf(docs):
    vocab = {}
    tfs = []
    for d in docs:
        vocab = get_term_frequency(d, vocab)
        tfs += [get_term_frequency(d)]

    from operator import itemgetter
    import numpy as np

    stats = []
    for word, freq in vocab.items():
        tf_v = []
        for idx in range(len(docs)):
            if tfs[idx].get(word) is not None:
                tf_v += [tfs[idx][word]]
            else:
                tf_v += [0]
        stats.append((word, freq, *tf_v))
    
    return pd.DataFrame(stats, columns=('word',
                                        'frequency',
                                        'doc1',
                                        'doc2',
                                        'doc3')).sort_values('frequency', ascending=False)

get_tf([doc1, doc2, doc3])​

<출력>
이때, 각 열들은 아래와 같다.
frequency: TF(w)
doc1: TF(w, d1)
doc2: TF(w, d2)
doc3: TF(w, d3)


TF(w, d1), TF(w, d2), TF(w, d3)는 문서에 대한 단어별 출현횟수를 활용한 특징벡터가 될 것이다.
예를들어 '여러분'은 [5, 6, 1]이라는 특징벡터를 갖는다.
문서가 많다면 지금보다 더 나은 특징벡터를 구할 수 있다.

다만, 문서가 지나치게 많으면 벡터의 차원 역시 지나치게 커질 수 있다.
문제가 되는 이유: 지나치게 커진 차원의 벡터 대부분의 값이 0으로 채워진다는 것.

위의 표에서 3개의 문서 중 일부만 출현해 TF가 0인 경우가 상당히 존재한다.
이렇게 벡터의 극히 일부분만 의미있는 값으로 채워진 벡터를 희소벡터라 한다.
희소벡터의 각 차원은 사실 대부분의 경우 0이기에 유의미한 특정통계를 얻기에 방지턱이 될 수 있다.
즉, 단순히 문서출현횟수로만 특징벡터를 구성하게되기에 많은 정보가 소실되었다.
( ∵ 매우 단순화되었기에 아주 정확한 특징벡터를 구성했다기엔 여전히 무리가 있다.)

 

6.2 context window 함께 출현한 단어들의 정보 활용하기
앞선 TF로 특징벡터를 구성한 방식보다 더 정확하다.

context window: 함께 나타나는 단어들을 조사하기 위해 windowing을 실행하여 그 안에 있는 unit들의 정보를 취합하는 방법에서 사용되는 window를 context window라 한다.
context window는 window_size라는 하나의 hyper-parameter가 추가되기에 사용자가 그 값을 지정해줄 수 있다.
 ∙ window_size가 지나치게 클 때: 현재 단어와 관계없는 단어들까지 TF를 count
 ∙ window_size가 지나치게 작을 때: 관계가 있는 단어들의 TF를 count


[문장들을 입력으로 받아 주어진 window_size내에 함께 출현한 단어들의 빈도를 세는 함수]
from collections import defaultdict

import pandas as pd

def get_context_counts(lines, w_size=2):
    co_dict = defaultdict(int)
    
    for line in lines:
        words = line.split()
        
        for i, w in enumerate(words):
            for c in words[i - w_size:i + w_size]:
                if w != c:
                    co_dict[(w, c)] += 1
            
    return pd.Series(co_dict)​

 

[TF-IDF를 위해 작성한 get_term_frequency()함수를 활용해 동시발생정보를 통해 벡터를 만드는 코드]
def co_occurrence(co_dict, vocab):
    data = []
    
    for word1 in vocab:
        row = []
        
        for word2 in vocab:
            try:
                count = co_dict[(word1, word2)]
            except KeyError:
                count = 0
            row.append(count)
            
        data.append(row)
    
    return pd.DataFrame(data, index=vocab, columns=vocab)​




<출력>

앞쪽의 출현빈도가 높은 단어들은 대부분의 값이 잘 채워져 있는 것을 알 수 있다.
뒤쪽의 출현빈도가 낮은 단어들은 대부분의 값이 0으로 채워져 있음을 알 수 있다. 

 

 

 

 

 

 


7. Vector Similarity (with Norm)

앞서 구한 특징벡터를 어떻게 사용할 수 있을까? 특징벡터는 단어간의 유사도를 구할 때 아주 유용하다.

가장 먼저 언급한 WordNet의 그래프구조에서 단어사이의 거리를 측정, 이를 바탕으로 단어사이의 유사도를 구하는 방법에 대해 알아보다.

 

이번에는 벡터간의 유사도 or 거리를 구하는 방법들을 다뤄볼 것이다.

 

 

 

7.1  L1 Distance (Manhattan Distance)
L1 Distance는 L1 norm을 사용하는 것으로 Manhattan Distance라고도 한다.
이 방법은 두 벡터의 각 차원별 값의 차이의 절대값을 모두 합한 값이다.

코드로 나타내면 아래와 같다.
def L1_distance(x1, x2):
    return ((x1-x2).abs()).sum()​

초록색선(L2 Distance)을 제외하고 나머지는 L1 Distance이다.

7.2  L2 Distance (Euclidean Distance)
L2 Distance란 L2 norm을 사용하는 것으로 Euclidean Distance라고도 한다.
이 방법은 두 벡터의 각 차원별 값 차이의 제곱의 합에 루트를 취한 값이다.



코드로 나타내면 아래와 같다.
def L2_distance(x1, x2):
    return ((x1 - x2)**2).sum()**.5​

 

7.3 Infinity Norm
Lnorm을 사용한 infinity distance는 차원별 값의 차이중 가장 큰 값을 나타낸다.

코드로 나타내면 아래와 같다.
def infinity_distance(x1, x2):
    return ((x1 - x2).abs()).max()​​

 

 

 

 

 

7.4 Cosine Similarity
코사인 유사도함수는 두 벡터사이의 방향과 크기를 모두 고려하는 방법이다.
특히, 자연어처리에서 가장 널리 쓰이는 유사도측정방법 이다.

∙ 수식에서 분자는 두 벡터간의 요소곱(element-wise)을 사용한다.(= 내적곱)
  - 코사인 유사도의 결과가 1에 가까울수록 방향은 일치하고
  - 코사인 유사도의 결과가 0에 가까울수록 직교이며
  - 코사인 유사도의 결과가 -1에 가까울수록 방향은 반대이다.


코드로 나타내면 아래와 같다.
def cosine_similarity(x1, x2):
    return (x1 * x2).sum() / ((x1**2).sum()**.5 * (x2**2).sum()**.5)​


[유의할 점]
다만, 분자의 벡터곱연산이나 분자의 L2 norm 연산은 cost가 높아 벡터차원이 커질수록 연산량이 커진다.
이는 희소벡터일 경우, 가장 큰 문제가 발생하는데, 해당차원이 직교하면 곱의 값이 0이되므로 정확한 유사도 및 거리를 반영하지 못한다.

 

7.5 Jarccard Similarity
jarccard similarity는 두 집합간의 유사도를 구하는 방법이다.


∙ 수식에서 분자는 두 집합의 교집합크기이고, 분모는 두 집합의 합집합니다.
  - 이때, 특징벡터의 각 차원이 집합의 요소(element)이다.
  - 다만, 각 차원에서의 값이 0 or 0이 아닌 값이 아닌, 수치 자체에 대해 jarccard similarity를 구하고자 할 때는, 2번째 줄의 수식처럼 두 벡터의 각 차원의 숫자에 대해 min, max연산으로 계산할 수 있다.

코드로 나타내면 아래와 같다.
def get_jaccard_similarity(x1, x2):
    return torch.stack([x1, x2]).min(dim=0)[0].sum() / torch.stack([x1, x2]).max(dim=0)[0].sum()​

 

7.6 문서 간 유사도 구하기
문서 = 문장의 집합 ; 문장 = 단어들의 집합
앞서 설명한 7절의 내용들은 단어에 대한 특징을 수집, 유사도를 구하는 방법이다.

따라서 문서에 대한 특징을 추출하여 문서간의 유사도를 구할 수 있는데, 
예를들어, 문서 내의 단어들에 대한 TF나 TF-IDF를 구해 벡터를 구성하고, 이를 활용해 벡터간의 유사도를 구할 수 있다.

물론, 현재 딥러닝에서는 훨씬 더 정확한 방법을 통해 문서간 또는 문장간 유사도를 구할 수 있다.

 

 

 

 

 

 


8. 단어 중의성 해소 (WSD)

8.1 Lesk Algorithm (Thesaurus 기반 WSD)
Lesk 알고리즘은 가장 간단한 사전 기반 중의성해소방법이다.
주어진 문장에서 특정단어의 의미를 명확히 할 때 사용할 수 있다.

∙ Lesk 알고리즘의 가정: 문장내 같이 등장하는 단어들은 공통 토픽을 공유
Lesk Algorithm

∙ 중의성을 해소하고자하는 단어에 대해 사전(wordnet)의 의미별 설명을 구함 

∙ 주어진 문장 내 등장단어의 사전에서 의미별 설명 사이 유사도를 구한다.
  주로 유사도를 구할 때, 단어의 개수를 구한다.
  
∙ 문장 내 단어들의 의미별 설명과 가장 유사도가 높은 의미를 선택한다.​



코드로 나타내면 아래와 같다.

def lesk(sentence, word):
    from nltk.wsd import lesk

    best_synset = lesk(sentence.split(), word)
    print(best_synset, best_synset.definition())





<예제>

In [26] 전까지는 잘 작동하지만, In [26]은 전혀 다른 의미로 예측하는 것을 볼 수 있다.
Lesk Algorithm은 명확한 장단점을 갖는다.
 - 장점) WordNet과 같이 잘 분류된 사전이 있다면, 쉽고 빠르게 WSD(단어 중의성 해소)문제를 해결할 수 있다.
 - 단점) 사전의 단어 및 의미에 관한 설명에 크게 의존하게되고, 설명이 부실하거나 주어진 문장에 큰 특징이 없다면 WSD능력이 크게 떨어진다.

 

 

 

 

 

 


9. Selection Preference

선택선호도(Selection Preference): 문장은 여러 단어의 시퀀스로 이뤄지는데, 문장 내 주변 단어들에 따라 의미가 정해지며 이를 더 수치화해 나타내는 방법이다. 이를 통해 WSD문제를 해결할 수 있다.

ex) '마시다'라는 동사에 대한 목적어로는 '음료'클래스에 속하는 단어가 올 가능성이 높기에 '차'라는 단어가 '음료'일지 '자가용'일지 어디에 속할 지 쉽게 알 수 있다.

 

9.1 Selection Preference Strength
선택선호도는 단어간 관계가 좀 더 특별한 경우를 수치화해 나타내는데
단어간 분포의 차이가 클수록 더 강력한 선택선호도를 갖는다
(= 선택 선호도 강도(strength)가 강하다).

이를 KLD(KL-Divergence)를 사용해 정의하였다.
선택선호도 강도 SR(w)은 w가 주어졌을 때, 목표클래스 C의 분포 P(C|w)와 그냥 해당 클래스들의 사전분포 P(C)와의 KLD로 정의되어 있음을 알 수 있다. (즉, 특정 클래스를 얼마나 선택적으로 선호하는지를 알 수 있다.)

 

9.2 Selectional Association
술어와 특정클래스사이 선택관련도를 살펴보자.
위의 수식에 따르면, 선택선호도강도가 낮은 술어에 대해 분자의 값이 클 경우, 술어와 클래스사이 더 큰 선택관련도를 갖는다는 것을 의미한다.
즉, 선택선호도강도가 낮아 해당 술어는 클래스에 대한 선택적선호강도가 낮지만,
특정클래스만 유독 술어에 영향을 받아 분자가 커져 선택관련도의 수치가 커질 수 있음을 내포한다.

 

9.3 Selection Preference. &. WSD
이런 선택선호도의 특징을 이용하면 WSD에 활용할 수 있다.
ex)  '마시다'라는 동사에 '치'라는 목적어가 함께 있을 때, '음료'클래스인지 '자가용'클래스인지만 알아내면 된다.

우리가 아는 corpus들은 단어로 표현되어 있을 뿐, 클래스로 표현되어 있지 않기에 
이를 위해 사전에 정의된 지식이나 dataset이 필요하다.
9.4 WordNet기반 Selection Preference
여기서 WordNet이 큰 위력을 발휘한다.
WordNet을 이용하면 '차(car)'의 상위어를 알 수 있고 이를 클래스로하여 필요정보를 얻을 수 있다.
술어와 클래스사이 확률분포를 정의하는 출현빈도의 계산수식을 아래와 같이 정의할 수 있다.

클래스 c에 속하는 표제어(headword), 즉 h는 실제 corpus가 나타난 단어로 술어(predicate) w와 함께 출현한 h의 빈도를 세고, h가 속하는 클래스들의 집합의 크기인 |Classes(h)|로 나누어준다.
그리고 이를 클래스 c에 속하는 모든 표제어에 대해 수행한 후 이를 합한 값을 CountR(w,c)를 근사한다.

이를 통해 술어 w와 표제어 h가 주어졌을 때 h의 클래스 c를 추정한 c_hat을 구할 수 있다.

 

9.5 pseudo word를 통한 Selection Preference평가
유사어휘(pseudo word)가 바로 정교한 testset설계를 위한 하나의 해답이 될 수 있다.
유사어휘는 2개의 단어가 인위적으로 합성되어 만들어진 단어이다.

 

9.6 Similarity Based Method[2007]
WordNet은 모든 언어에 존재하지 않고, 새롭게 생겨나는 신조어들도 반영되지 않을 가능성이 높다.
따라서 wordnet과 thesaurus에 의존하지 않고 선택선호도를 구할 수 있다면??

이를 위해 thesaurus나 thesaurus기반 Lesk알고리즘에 의존하지 않고 data를 기반으로 간단하게 선택선호도를 구하는 방법을 알아보자.
술어 w, 표제어 h, 두 단어 사이의 관계 R이 tuple로 주어질 때, 선택관련도를 아래와 같이 정의할 수 있다.

이때, 가중치 ØR(w,h)는 동일하게 1이나 아래처럼 IDF를 사용해 정의할 수도 있다.

또한 sim함수는 일전의 코사인유사도나 jarccard유사도를 포함, 다양한 유사도함수를 사용할 수 있다.
유사도비교를 위해 SeenR(w)함수로 대상단어를 선정하기에 corpus에 따라 유사도를 구할 수 있는 대상이 달라지며 이를 통해 thesaurus없이도 쉽게 선택선호도를 계산할 수 있게 된다.

 

유사도기반 선택선호도 예제

from konlpy.tag import Kkma

with open('ted.aligned.ko.refined.tok.random-10k.txt') as f:
    lines = [l.strip() for l in f.read().splitlines() if l.strip()]

def count_seen_headwords(lines, predicate='VV', headword='NNG'):
    tagger = Kkma()
    seen_dict = {}
    
    for line in lines:
        pos_result = tagger.pos(line)
        
        word_h = None
        word_p = None
        for word, pos in pos_result:
            if pos == predicate or pos[:3] == predicate + '+':
                word_p = word
                break
            if pos == headword:
                word_h = word
        
        if word_h is not None and word_p is not None:
            seen_dict[word_p] = [word_h] + ([] if seen_dict.get(word_p) is None else seen_dict[word_p])
            
    return seen_dict
def get_selectional_association(predicate, headword, lines, dataframe, metric):
    v1 = torch.FloatTensor(dataframe.loc[headword].values)
    seens = seen_headwords[predicate]
    
    total = 0
    for seen in seens:
        try:
            v2 = torch.FloatTensor(dataframe.loc[seen].values)
            total += metric(v1, v2)
        except:
            pass
        
    return total
def wsd(predicate, headwords):
    selectional_associations = []
    for h in headwords:
        selectional_associations += [get_selectional_association(predicate, h, lines, co, get_cosine_similarity)]

    print(selectional_associations)

'가'라는 조사에 가장 잘 맞는 단어가 '학교'임을 잘 예측하는 것을 알 수 있따.

 

 

 


마치며...

단어는 보기에는 불연속적인 형태이지만 내부적으로는 연속적인 '의미(sense)'를 갖는다.
따라서 우린 단어의 겉모양이 다르더라도 의미가 유사할 수 있음을 알고 있으며,
단어의 유사도를 걔산할 수 있더라면 corpus로부터 분포나 특징을 훈련 시, 더 정확한 훈련이 가능하다.


WordNet이라는 사전의 등장으로 단어사이 유사도(거리)를 계산할 수 있게 되었지만
WordNet과 같은 Thesaurus 구축은 엄청난 일이기에 사전없이 유사도를 구하면 더 좋을 것이다.

사전없이 corpus만으로 특징을 추출해 단어를 벡터로 만든다면, WordNet보다 정교하지는 않지만 더 간단한 작업이 될 것이다.
추가적으로 corpus의 크기가 커질수록 추출할 특징들은 점차 정확해질 것이고, 특징벡터도 더 정확해질 것이다.

특징벡터가 추출된 후 cosine유사도, L2 Distance 등의 Metric을 통해 유사도를 계산할 수 있다.


하지만 앞서 서술했듯, 단어사전 내 단어 수가 많은만큼 특징벡터의 차원도 단어사전크기와 같기에 
"단어대신 문서"를 특징으로 사용하더라도 주어진 문서의 숫자만큼 벡터의 차원이 생성될 것이다.

다만 더 큰 문제는 그 차원의 대부분 값이 0으로 채워지는 "희소벡터"로 
무엇인가 학습하고자 할 때, cosine유사도의 경우 직교하여 유사도값을 0으로 만들 수 있다.
즉, 정확한 유사도를 구하기 어려워 질 수 있다.

이런 희소성문제는 NLP의 두드러진 특징으로 단어가 불연속적 심볼로 이뤄지기 때문이다.
전통적인 NLP는 이런 희소성문제에 큰 어려움을 겪었다.


하지만 최신 딥러닝에서는 단어의 특징벡터를 이런 희소벡터로 만들기보다는
"word embedding"기법으로 0이 잘 존재하지 않는 dense 벡터로 만들어 사용한다.
ex) word2vec, GloVe, ... (https://chan4im.tistory.com/197)

 

📌 목차

0. preprocessing
1. corpus 수집
2. Normalization

3. 문장단위 tokenization
4. tokenization (한국어, 영어, 중국어)
5. parallel corpus 정렬
6. subword tokenization (BPE Algorithm  &  UK, OOV)
7. Detokenization (분절화 복원)
8. torchText 라이브러리

 

 


0. Preprocessing

0.1 corpus
corpus는 말뭉치라고도 불리며 보통 여러단어들로 이루어진 문장을 의미.
train data를 위해 이런 다수의 문장으로 구성된 corpus가 필요하다.

 ∙ monolingual corpus: 한 가지 언어로 구성된 corpus
 ∙ bilingual corpus: 두 개의 언어로 구성된 corpus
 ∙ multilingual corpus: 더 많은 수의 언어로 구성된 corpus
 ∙ parallel corpus: 언어간의 쌍(ex.영문-한글)으로 구성된 corpus

 

0.2 preprocessing 과정 개요
corpus 수집
Normalize
문장단위 Tokenize
Tokenize
parallel corpus 정렬
subword tokenize

 

 

 

 


1. Corpus 수집

corpus를 수집하는 방법은 여러가지가 있는데, 무작정 웹사이트에서 corpus 크롤링을 하면 법적 문제가 될 수 있다.

저작권은 물론, 불필요한 트래픽이 웹서버에 가중되는 과정에서 문제발생가능

따라서 적절한 웹사이트에서 올바른 방법 or 상업적 목적이 아닌 경우로 제한된 크롤링이 권장된다.

해당 웹사이트의 크롤링 허용여부는 사이트의 robots.txt를 보면 된다.

ex) TED의 robot.txt 확인방법

 

1.1 monolingual corpus 수집
가장 쉽게 구할 수 있는 corpus(인터넷에 그냥 널려있기 때문)이기에 대량의 corpus를 손쉽게 얻을 수 있다.
다만, 올바른 도메인의 corpus를 수집, 사용가능한 형태로 가공하는 과정이 필요하다.

 

1.2 multilingual corpus 수집
자동 기계번역을 위한 parallel corpus를 구하기는 monolingual corpus에 비해 상당히 어렵고
'he'와 'she'와 같은 대명사가 사람이름 등의 고유명사로 표현될 때가 많아 이런 번역에 대한 문제점들을 두루 고려해야한다.

 

 

 

 


 

2. Normalization

2.1 전각/반각 문자 제거

대부분의 일본/중국어 문서와 일부 한국어문서의 숫자, 영자, 기호가 전각문자일 가능성이 존재하기에 이를 반각 문자로 변환해주는 작업이 필요하다.

 

 

2.2 대소문자 통일
다양한 표현의 일원화는 하나의 의미를 갖는 여러 단어를 하나의 형태로 통일해 희소성(sparsity)을 줄여준다.
다만, 딥러닝이 발전하면서 다양한 단어들을 비슷한 값의 vector로 단어임베딩으로 대소문자 해결의 필요성이 줄어들었다.

 

2.3 정규 표현식을 사용한 Normalize
크롤링으로 얻은 다량의 corpus의 경우, 특수문자 및 기호 등에 의한 Noise가 존재한다.
또한 웹사이트의 성격에 따른 일정 패턴을 갖는 경우도 있기에
효율적으로 noise를 감지, 없애야 하는데, 이때 인덱스의 사용이 필수적이다.
아래는 정규식시각화사이트(https://regexper.com/)를 활용해 나타낸 것이다.
[ ] 사용

2 or 3 or 4 or 5 or c or d or e와 같은 의미를 갖는다.
면 아래의 좌측과 같다.
ex) [2345cde]

- 사용
연속된 숫자나 알파벳 등을 표현할 수 있다.
ex) [2-5c-e]

^ 사용
not을 기호 ^을 사용해 표현할 수 있다.
ex) [2-5c-e]

( ) 사용
괄호를 이용해 group 생성할 수 있다.
ex) (x)(yz)




? + * 사용
?: 앞의 수식하는 부분이 나타나지 않거나 한번만 나타날 때
+: 앞의 수식하는 부분이 한번 이상 나타날 경우
*: 앞의 수식하는 부분이 나타나지 않거나 여러번 나타날 경우

ex) x?
ex) x+
ex) x*



^와 $의 사용

[ ] 내에 포함되지 않을 때,
^라인의 시작을 의미
$라인의 종료를 의미
ex) ^x$

 

예제 1)

아래의 개인정보(전화번호)가 포함된 corpus를 dataset으로 사용할 때, 개인정보를 제외하고 사용하고자 한다면?

단, 항상 마지막 줄에 전화번호 정보가 있는 것은 아니라면?

Hello Kim, I would like to introduce regular expression in this section
~~
Thank you!
Sincerely,

Kim: +82-10-1234-5678

 

개인정보의 규칙을 먼저 파악해보자. 국가번호는 최대 3자리이고 앞에 +가 붙을 수 있으며 전화번호 사이에 -가 들어갈 수도 있다.

전화번호는 빈칸 없이 표현되며 지역번호가 들어갈 수 있고 마지막은 항상 4자리 숫자이다 등등...

import re
regex = r"([\w]+\s*:?\s*)?\(?\+?([0-9]{1,3})?\-[0-9]{2,3}(\)|\-)?[0-9]{3,4}\-?[0-9]{4}"

x = "Name - Kim: +82-10-9425-4869"
re.sub(regex, "REMOVED", x)

출력: Name - REMOVED

 

 

예제 2) 치환자 사용

아래의 예제에서 알파벳 사이에 있는 숫자를 제거해야한다면?

만약 단순히 [0-9]+ 로 숫자를 찾아 없앤다면 숫자만 있거나 숫자가 가장자리에 있는 경우도 사라지게 된다.

x = '''abcdefg
12345
ab12
12ab
a1bc2d
a1
1a'''

 

따라서 괄호로 group을 생성하고 바뀔 문자열 내에서 역슬래시(\)와 함께 숫자를 이용해 마치 변수명처럼 가리킬 수 있다.

regex = r'([a-z])[0-9]+([a-z])'
to = r'1\2\'

y = '\n'.join([re.sub(regex, to, x_i) for x_i in x.split('\n')])

([a-z])[0-9]+([a-z])

 

 

 

 

 

 

 


3. 문장단위 tokenization

다만, 보통 다루려는 문제들은 입력단위가 아닌, 문장단위인 경우가 많고

대부분의 경우, 한 라인에 한 문장만 있어야 한다.

따라서 여러 문장이 한 라인에 있거나 한 문장이 여러 라인에 걸쳐있다면, 이를 분절(tokenize)해줘야 한다.

이를 위해 직접 분절하는 알고리즘을 만들기보다는 널리 알려진 NLP Toolkit, NLTK에서 제공하는 sent_tokenize 이용이 주가 된다.

물론, 추가적인 전처리 및 후처리가 필요한 경우들도 존재한다.

3.1 sentence tokenization 예제
import sys, fileinput, re
from nltk.tokenize import sent_tokenize

if __name__ == "__main__":
    for line in fileinput.input():
        if line.strip() != "":
            line = re.sub(r'([a-z])\.([A-Z])', r'\1. \2', line.strip())

            sentences = sent_tokenize(line.strip())

            for s in sentences:
                if s != "":
                    sys.stdout.write(s + "\n")​

 

3.2 문장 합치기 및 분절 예제
import sys, fileinput
from nltk.tokenize import sent_tokenize

if __name__ == "__main__":
    buf = []

    for line in fileinput.input():
        if line.strip() != "":
            buf += [line.strip()]
            sentences = sent_tokenize(" ".join(buf))

            if len(sentences) > 1:
                buf = sentences[1:]

                sys.stdout.write(sentences[0] + '\n')

    sys.stdout.write(" ".join(buf) + "\n")​

 

 

 

 


4. Tokenization

풀고자 하는 문제, 언어에 따라 형태소 분석, 단순한 분절을 통한 정규화를 수행하는데, 특히 띄어쓰기에 관해 살펴보자.

 

한국어의 경우, 표준화 과정이 충분하지 않아 띄어쓰기가 지멋대로인 경우가 상당히 많으며, 띄어쓰기가 문장해석에 큰 영향을 주지 않아 이런 현상이 더욱 더 가중되는 경향이 존재한다. 따라서 한국어의 경우, 정규화를 해줄 때, 표준화된 띄어쓰기를 적용하는 과정도 필요하며 교착어로써 접사를 어근에서 분리해주는 역할도 필요하기에 희소성문제를 해소하기도 한다.
이런 한국어의 tokenization을 위한 프로그램으로는 C++로 제작된 Mecab, Python으로 제작된 KoNLPy가 전처리를 수행한다.
https://github.com/kh-kim/nlp_with_pytorch_examples/blob/master/chapter-04/tokenization.ipynb


영어
의 경우, 기본적으로 띄어쓰기가 있고, 기본적으로 대부분의 경우 규칙을 매우 잘 따르고 있다.
영어의 경우, 보통 앞서 언급했듯 기본적인 띄어쓰기가 잘 통일되어 있는 편이므로 띄어쓰기 자체에는 큰 정규화 문제가 존재하지 않는다.
다만 쉼표(comma), 마침표(period), 인용부호(quotation) 등을 띄어주어야 하므로 Python으로 제작된 NLTK를 이용한 전처리를 수행한다.
$ pip install nltk==3.2.5​


일본어와 중국어의 경우, 모든 문장이 띄어쓰기가 없는 형태를 하고 있지만, 적절한 언어모델구성을 위해 띄어쓰기가 필요하다.
일본어의 경우, C++ base의 Mecab을, 중국어의 경우 Java base의 Stanford Parser, PKU Parser를 사용한다.

 

 

 

 

 

 

 


5. 병렬 Corpus 정렬

예를들어 영어신문과 한글신문이 맵핑되는, 문서와 문서단위 맵핑의 경우,

문장 대 문장에 관한 정렬은 이루어져 있지 않았기에 일부 불필요한 문장들을 걸러내야한다.

 

5.1 parallel corpus 제작과정 개요
 source와 target 언어간의 단어사전을 준비,
  준비된 단어사전이 있으면 으로 이동, 없다면 아래과정을 따른다.

각 언어에 대한 corpus를 수집 및 정제

각 언어에 대한 word embedding vector를 구한다.

MUSE를 통해 단어레벨 번역기를 훈련
https://github.com/facebookresearch/MUSE

훈련된 단어레벨 번역기를 통해 두 언어간의 단어사전을 생성

 만들어진 단어사전을 넣어 Champollion을 통해 기존에 수집된 multilingual corpus를 정렬
https://github.com/LowResourceLanguages/champollion

각 언어에 대해 단어사전을 적용하기 위해 적절한 tokenization을 수행

각 언어에 대해 Normalization을 수행

Champollion을 사용해 parallel corpus를 생성

 

5.2 단어사전 생성
facebookMUSE는 parallel corpus가 없는 상황에서 사전을 구축하는 방법과 코드를 제공한다.

MUSE는 각 monolingual corpus를 통해 구축된 언어별 word embedding vector에 대해
다른 언어의 embedding vector와 mapping시켜 단어 간의 번역을 수행할 수 있는, unsupervised learning이다.
<>를 구분문자(delimeter)로 사용해 한 라인에 source단어와 target단어를 표현한다.

ex)  MUSE를 통해 unsupervised learning을 사용해 결과물로 얻은 영한 단어 번역사전의 일부.
상당히 정확한 단어간 번역을 볼 수 있다.
stories <> 이야기
stories <> 소설
contact <> 연락
contact <> 연락처
contact <> 접촉
green <> 초록색
green <> 빨간색
dark <> 어둠
dark <> 짙

 

5.3 CTK를 활용한 정렬
앞서 구성한 사전은 CTK의 입력으로 사용되는데, CTK는 이 사전을 바탕으로 parallel의 문장정렬을 수행한다.
CTK는 bilingual corpus의 문장정렬을 수행하는 오픈소스로 Perl을 사용해 구현되었다.

기존 혹은 자동으로 구축된 단어사전을 참고해 CTK는 문장정렬을 수행하는데, 여러 라인으로 구성된 언어별 문서에 대해 문장 정렬한 결과의 예제는 아래와 같다.
omitted <=> 1
omitted <=> 2
omitted <=> 3
1 <=> 4
2 <=> 5
3 <=> 6
4,5 <=> 7
6 <=> 8
7 <=> 9
8 <=> 10
9 <=> omitted

해석) target언어의 1,2,3번째 문장은 짝을 찾지 못해 버려짐
해석) source언어의 1,2,3번째 문장은 target의 4,5,6번째 문장과 mapping
해석) source언어의 4,5번째 두 문장은 target의 7번 문장에 동시에 mapping

 

위의 예시처럼 어떤 문장들은 버려지기도 하고
일대일(one-to-one)맵핑, 일대다(one-to-many), 다대일(many-to-one)맵핑이 이뤄지기도 한다.





ex) CTK를 쉽게 사용하기 위해 파이썬으로 감싼 스크립트 예제
이때, CTK_ROOT에 CTK 위치를 지정해 사용할 수 있다.
import sys, argparse, os

BIN = "NEED TO BE CHANGED"
CMD = "%s -c %f -d %s %s %s %s"
OMIT = "omitted"
DIR_PATH = './tmp/'
INTERMEDIATE_FN = DIR_PATH + "tmp.txt"

def read_alignment(fn):
    aligns = []

    f = open(fn, 'r')

    for line in f:
        if line.strip() != "":
            srcs, tgts = line.strip().split(' <=> ')

            if srcs == OMIT:
                srcs = []
            else:
                srcs = list(map(int, srcs.split(',')))

            if tgts == OMIT:
                tgts = []
            else:
                tgts = list(map(int, tgts.split(',')))

            aligns += [(srcs, tgts)]

    f.close()

    return aligns

def get_aligned_corpus(src_fn, tgt_fn, aligns):
    f_src = open(src_fn, 'r')
    f_tgt = open(tgt_fn, 'r')

    for align in aligns:
        srcs, tgts = align

        src_buf, tgt_buf = [], []

        for src in srcs:
            src_buf += [f_src.readline().strip()]
        for tgt in tgts:
            tgt_buf += [f_tgt.readline().strip()]

        if len(src_buf) > 0 and len(tgt_buf) > 0:
            sys.stdout.write("%s\t%s\n" % (" ".join(src_buf), " ".join(tgt_buf)))

    f_tgt.close()
    f_src.close()

def parse_argument():
    p = argparse.ArgumentParser()

    p.add_argument('--src', required = True)
    p.add_argument('--tgt', required = True)
    p.add_argument('--src_ref', default = None)
    p.add_argument('--tgt_ref', default = None)
    p.add_argument('--dict', required = True)
    p.add_argument('--ratio', type = float, default = 1.1966)

    config = p.parse_args()

    return config

if __name__ == "__main__":
	assert BIN != "NEED TO BE CHANGED"

    if not os.path.exists(DIR_PATH):
         os.mkdir(DIR_PATH)

    config = parse_argument()

    if config.src_ref is None:
        config.src_ref = config.src
    if config.tgt_ref is None:
        config.tgt_ref = config.tgt

    cmd = CMD % (BIN, config.ratio, config.dict, config.src_ref, config.tgt_ref, INTERMEDIATE_FN)
    os.system(cmd)

    aligns = read_alignment(INTERMEDIATE_FN)
    get_aligned_corpus(config.src, config.tgt, aligns)

 

 

 

 

 

 

 


6. Subword Tokenization (with Byte Pair Encoding)

6.1 BPE (Byte Pair Algorithm)
BPE를 통한 subword tokenization은 현재 가장 필수적인 전처리방법이다.
ex) concentrate = con(together) + centr(=center) + ate(=make)
ex) 집중(集中) = (모을 집) + (가운데 중)

[subword tokenization 알고리즘]
 - 단어는 의미를 갖는 더 작은 subwords의 조합으로 이뤄진다는 가정하에
 - 적절한 subword를 발견해 해당 단위로 쪼개어
 - 어휘 수를 줄이고 sparsity를 효과적으로 줄이는 방법.
 - 특히 UNK(Unknown) Token에 대해 효율적 대처이다.
[UNK Token , OOV. &.  BPE]
∙ U.K[Unknown Token]:
train corpus에 없는 단어
∙ OOV[Out-of-Vocabulary] 문제: U.K로 인해 문제를 푸는 것이 까다로워지는 현상.

자연어처리에서 문장을 입력으로 받을 때 단순히 단어들의 시퀀스로 받기에

UNK Token은 자연어처리 모델의 확률을 망가뜨리고 적절한 embedding(encoding) 또는 생성이 어려워지는 지뢰이다.
특히, 문장생성의 경우, 이전단어를 기반으로 다음 단어를 예측하기에 더욱 어려워진다.
subword 분리로 OOV문제를 완화하는데, 가장 대표적인 subword 분리알고리즘이 바로 BPE(Byte Pair Encoding)
이다.

하지만 subword단위의 tokenization을 진행하는 BPE 알고리즘의 경우, 신조어나 오타(typo)같은 UNK Token에 대해 subword 단위나 문자(character)단위로 쪼개 기존 train data의 token들의 조합으로 만들어버릴 수 있다.
즉, UNK 자체를 없앰으로써 효율적으로 UNK에 대처하여 성능을 향상시킬 수 있다.

ex)

[영어 NLTK]에 의해 분절된 원문
Natural language processing is one of biggest streams in A.I


[영어 BPE]로 subword로 분절된 원문
_Natural _language _processing _is _one _of _biggest _stream s _in _A. I

 

 

 

 

 

 


7. Detokenization

전처리과정에서 tokenization을 수행하였으면, 다시 detokenization을 수행해줘야한다.

즉, 아래와 같은 전처리과정을 따른다.

∙ 언어별 tokenizer모듈(ex. NLTK)로 분절 수행
 이때, 새롭게 분절되는 공백과의 구분을 위해 기존 공백에 _ 기호를 삽입

∙ subword단위 tokenization(ex. BPE알고리즘)을 수행
 이때, 이전 과정까지의 공백과 subword단위 분절로 인한 공백 구분을 위해 특수문자 _ 기호를 삽입
 즉, 기존 공백의 경우 _ _를 단어 앞에 갖게 되는 것.

∙ Detokenize 진행
 먼저 공백을 제거한다.
 이후 (_를 2개 갖는)_ _ 문자열을 공백으로 치환한다.
 마지막으로 _ 를 제거한다.

 

7.1 tokenization 후처리
Detokenization을 쉽게하기 위해 tokenize 이후 특수문자를 분절과정에서 새롭게 생긴 공백 다음에 삽입해야한다.


예제) 기존 공백과 전처리단계에서 생성된 공백을 서로 구분하기 위한 특수문자 _ 를 삽입하는 코드

tokenizer.py
import sys

STR = '▁'

if __name__ == "__main__":
    ref_fn = sys.argv[1]

    f = open(ref_fn, 'r')

    for ref in f:
        ref_tokens = ref.strip().split(' ')
        input_line = sys.stdin.readline().strip()

        if input_line != "":
            tokens = input_line.split(' ')

            idx = 0
            buf = []

            # We assume that stdin has more tokens than reference input.
            for ref_token in ref_tokens:
                tmp_buf = []

                while idx < len(tokens):
                    if tokens[idx].strip() == '':
                        idx += 1
                        continue

                    tmp_buf += [tokens[idx]]
                    idx += 1

                    if ''.join(tmp_buf) == ref_token:
                        break

                if len(tmp_buf) > 0:
                    buf += [STR + tmp_buf[0].strip()] + tmp_buf[1:]

            sys.stdout.write(' '.join(buf) + '\n')
        else:
            sys.stdout.write('\n')

    f.close()​

 

7.2 Detokenize 예제
위의 스크립트(tokenizer.py)를 사용하는 방법은 아래와 같다.
주로 다른 분절모듈의 수행 후에 pipe를 사용해 붙여서 사용한다.
$ cat [before filename] | python tokenizer.py | python post_tokenizer.py [before filename]​


post_tokenizer.py
import sys

if __name__ == "__main__":
    for line in sys.stdin:
        if line.strip() != "":
            if '▁▁' in line:
                line = line.strip().replace(' ', '').replace('▁▁', ' ').replace('▁', '').strip()
            else:
                line = line.strip().replace(' ', '').replace('▁', ' ').strip()

            sys.stdout.write(line + '\n')
        else:
            sys.stdout.write('\n')​

 

 

 

 

 

 

 


8. Torchtext 라이브러리

8.1 Torchtext 란?
Torchtext 라이브러리는 자연어처리를 수행하는 data를 읽고 전처리하는 코드를 모아둔 라이브러리이다.
(https://pytorch.org/text/stable/index.html)
Torchtext라이브러리를 활용하면 쉽게 text파일을 읽어내 훈련에 사용할 수 있다.


자세한 설명은 아래 링크(https://wikidocs.net/60314)를 참고
 

08-02 토치텍스트 튜토리얼(Torchtext tutorial) - 영어

파이토치(PyTorch)에서는 텍스트에 대한 여러 추상화 기능을 제공하는 자연어 처리 라이브러리 토치텍스트(Torchtext)를 제공합니다. 자연어 처리를 위해 토치텍스트가 반드…

wikidocs.net

 

Pytorch Tutorials

https://tutorials.pytorch.kr/

 

파이토치(PyTorch) 한국어 튜토리얼에 오신 것을 환영합니다!

파이토치(PyTorch) 한국어 튜토리얼에 오신 것을 환영합니다. 파이토치 한국 사용자 모임은 한국어를 사용하시는 많은 분들께 PyTorch를 소개하고 함께 배우며 성장하는 것을 목표로 하고 있습니다.

tutorials.pytorch.kr

 

 

 

Pytorch_Brief Tutorials

 

0. Why PyTorch?

🤔 Tensorflow:
∙Google에서 개발
∙딥러닝전용하드웨어인 TPU를 갖고 있어 GPU에서 상대적으로 자유로움

🤔 Pytorch:
∙facebook의 주도하에 개발, 
∙Nvidia의 cuda GPU에 더욱 최적화
∙Python First, 깔끔한 코드, numpy와의 호환성, Autograd, Dynamic Graph 등의 장점

 

 

1. Tensor

pytorch의 tensor는 numpy의 배열인 ndarray와 같은 개념으로
pytorch에서 연산을 수행하기 위한 가장 기본적인 객체이다. (앞으로의 모든 연산은 이 tensor 객체를 통해서 진행)

ex) numpy와 pytorch는 굉장히 비슷한 방식의 코딩스타일을 갖는다.
(자료형의 경우에도 torch.uint8과 같이 표기가 가능)
import numpy as np
x = np.array([[1,2], [3,4]])

import torch
x = torch.Tensor([[1,2], [3,4]])

cf) torch.Tensor는 default로 float32를 갖는다.

 

 

2. Autograd

Autograd는 자동으로 미분 및 역전파를 수행하는 기능이다.
즉, 대부분의 tensor간의 연산을 크게 신경 쓸 필요 없이 역전파알고리즘 수행 명령어를 호출할 수 있다.

이때, tensor간의 연산을 수행할 때마다 동적으로 computational graph를 생성하며,
연산의 결과물이 어떤 tensor로부터 어떤 연산을 통해 왔는지 또한 추적한다.
→ 결과적으로 최종 스칼라값에 역전파를 통한 미분을 수행 시, 각 tensor는 자식노드에 해당하는 tensor와 연산을 자동으로 찾아 역전파알고리즘을 계속 수행할 수 있게한다.


🤔 기존 keras 및 tensorflow와 다른점??
keras tensorflow 미리 정의한 연산들을 컴파일을 통해 고정, 정해진 입력에 맞춰 tensor를 순전파 시켜야 한다.

반면, Pytorch 정해진 연산이 없고 모델은 배워야 하는 parameter tensor만 미리 알고있다
즉, 가중치들이 어떠한 연산을 통해 학습 or 연산에 관여하는지 알 수 없고, 연산이 수행된 직 후 알 수 있다.

기울기를 구할 필요가 없는 연산의 경우, 다음과 같은 with 문법을 사용해 연산을 수행할 수 있는데, 이는 prediction과 inference등을 수행할 때 유용하며, 기울기를 구하기 위한 computational graph 생성 등의 사전작업을 생략하여 연산속도 및 메모리 사용측면에서도 큰 이점이 존재한다.
with torch.no_grad():
	z = (x+y) + torch.Tensor(2,2)​

 

 

3.  nn.Module

nn.Module 클래스는 사용자가 그 위에서 필요한 모델 구조를 구현할 수 있게 해준다.
nn.Module을 상속한 사용자클래스는 다시 내부에 nn.Module을 상속한 클래스객체를 선언 및 변수로 사용할 수 있다.

ex) Feed Forward 구현
import torch
import torch.nn as nn

def linear(x, W, b):
	return torch.mm(W, x) + b

class MyLinear(nn.Module):
	def __init__(self, input_size, output_size):
    	super().__init__()
        	self.W = torch.FloatTensor(input_size, output_size)
            self.b = torch.FloatTensor(output_size)
   
   	def forward(self, x):
    	y = torch.mm(self.W, x) + self.b
        return y
        
        
        
x = torch.Tensor(16, 10)
linear = MyLinear(10, 5)
y = linear(x)​
x ∈ R16×10
W ∈ R 10×5
b ∈ R5

>>> print([p.size() for p in linear.parameters()])
>>> [ ]​
다만, 현재 parameter(W, b)의 경우, [ ]로 학습가능한 파라미터가 없다고 출력된다.
따라서 Parameter 클래스를 사용해 tensor를 감싸야 한다.

import torch
import torch.nn as nn

def linear(x, W, b):
	return torch.mm(W, x) + b

class MyLinear(nn.Module):
	def __init__(self, input_size, output_size):
    	super().__init__()
        	self.W = nn.Parameter(torch.FloatTensor(input_size, output_size), requires_grad=True)
            self.b = nn.Parameter(torch.FloatTensor(output_size), requires_grad=True)
   
   	def forward(self, x):
    	y = torch.mm(self.W, x) + self.b
        return y
        
        
        
x = torch.Tensor(16, 10)
linear = MyLinear(10, 5)
y = linear(x)​​

위의 경우, 출력값으로 [torch.Size([10, 5]), torch.Size([5])] 가 출력된다.

 

 

4.  train()과 eval()

Backpropagation Algorithm의 경우, backward()함수를 이용해 진행가능하며, 이때 loss함수를 앞에 붙이는 형태로 표현한다.
loss.backward()​



train과 eval함수는 모델에 대한 training time과 inference time의 모드를 쉽게 전환할 수 있는 방법이다.
nn.Module을 상속받아 구현∙생성된 객체는 기본적으로 train모드로 이를 eval()을 사용해 추론모드로 바꿀 수 있다.
이는 Dropout, Batch Norm 같은 학습시와 추론 시 서로 다른 forward()동작을 하는 모듈들에도 올바르게 동작할 수 있다.
다만, 추론이 끝나면 다시 train()을 선언 해 원래의 train모드로 돌아가줘야 한다.

 

 

5.  GPU 사용하기

cuda() 함수
① 원하는 tensor객체를 GPU메모리에 복사하거나 
② nn.Module의 하위클래스를 GPU메모리로 이동시킬 수 있다.
x = torch.cuda.FloatTensor(16, 10)

linear = MyLinear(10, 5)
linear.cuda()

y = linear(x)​


cpu() 함수
다시 PC의 메모리로 복사하거나 이동시킬 수 있다.

to() 함수
tensor 또는 모듈을 원하는 device로 보낼 수 있다.

 

 

 

 

6.  Pytorch에서 DNN을 수행하는 과정 요약 및 예시

 nn.Module 클래스를 상속받아 forward함수를 통해 모델구조 선언
 해당 클래스 객체 생성
 Optimizer 생성, 생성한 모델의 parameter를 최적화대상으로 등록
 Data로 mini-batch를 구성, 순전파 연산그래프 생성
 손실함수를 통해 최종결과값, 손실값 계산
 손실에 대해서 backward() 호출
  → 이를통해 연산그래프 상의 tensor들의 gradient가 채워짐
 ③의 optimizer에서 step()을 호출, 1 step 수행
 ④로 돌아가 반복

 

 

 

 

 

 

 

7.  Pytorch 실습

[Linear Regression 분석]

📌 조건

∙ 임의로 tensor를 생성, 근사하고자하는 정답함수(wx+b)에 넣어 정답(y)을 구함

∙ 신경망 통과한 y_hat과의 차이를 구함(이때, 오류함수는 MSE Loss function을 사용)

∙ SGD를 이용해 optimization 진행

 

 

❗️ 1개의 Linear Layer를 갖는 MyModel이라는 모듈 선언

import random
import torch
import torch.nn as nn

class MyModel(nn.Module):
	def __init__(self, input_size, output_size):
    	super(MyModel, self).__init__()
        self.linear = nn.Linear(input_size, output_size)
        
    def forward(self, x):
    	y = self.linear(x)
        return y

 

 

 

❗️ Wx + b가 3x1 + x2 - 2x3에 근사한다 가정하자.

def ground_truth(x):
	return 3*x[:,0] + x[:,1] - 2*x[:,2]

 

 

 

❗️ Hyper-Parameter 설정

batch_size = 1
epoch = 1000
iter = 10000

model = Mymodel(3, 1)
optim = torch.optim.SGD(model.parameters(), lr=0.0001, momentum=0.1)

 

 

 

❗️ Model과 tensor를 입력받아 순전파 후, 역전파알고리즘을 수행해 경사하강법 1 step을 진행

def train(model, x, y, optim):
	# 모듈의 모든 파라미터의 기울기 초기화
    optim.zero_grad()
    
    # Feed-Forward
    y_hat = model(x)
    
    # MSE Loss
    loss = ((y-y_hat)**2).sum() / x.size(0)
    
    # BP Algorithm
    loss.backward()
    
    # GD 1 step
    optim.step()
    
    return loss.data

 

 

 

 

❗️ Train & Inference time

for epoch in range(epoch):
	# train 설정
	avg_loss = 0
    
    for i in range(iter):
    	x = torch.rand(batch_size, 3)
        y = ground_truth(x.data)
        
        loss = train(model, x, y, optim)
        
        avg_loss += loss
        avg_loss = avg_loss / iter
        
        
        
    # valid 설정
    x_val = torch.FloatTensor([[.3, .2, .1]])
    y_val = ground_truth(x_val.data)
    
    
    
    # inference 
    model.eval()
    y_hat = model(x_val)
    model.train()
    
    print(avg_loss, y_val.data[0], y_hat.data[0, 0])
    
    
    # finish
    if avg_loss < .001:
    	break

 

'Gain Study' 카테고리의 다른 글

[Gain Study]. inference time, Mobilenet, 배포  (0) 2023.08.19

다루는 내용

∙ 모델 training 및 inference의 성능에 영향을 미치는 요인

∙ 성능향상을 위한 다양한 방법

배포를 위한 클라우드 플랫폼(AWS, GCP)

다양한 모바일 플랫폼을 이용한 배포(Jetwon TX2, Android, iOS)

 

 


RAM과 같은 메모리 제약이 있는 mobile device들에서 결과모델(resultant model)의 사용은 문제가 될 수 있는데,

computing time이 증가할수록 infra cost가 증가한다.

 

특히, inference time은 동영상과 같은 application에서 매우 중요한데,

모델의 복잡성을 낮추기는 쉬우나 이는 정확도 또한 같이 떨어지게 되는 결과를 초래할 가능성이 높다.

 

따라서, 복잡성을 낮추지만 정확도를 심각하게 떨어뜨리지 않으면서 성능을 향상 혹은 유지시키는 방법이 중요하다.

이에 대한 대표적인 예시로 모델 양자화(Model Quantization)에 대해 설명하겠다.

 


🤔 Model Quantization (모델 양자화)

1. Quantization ?
물리학에서 양자화(Quantization)란, 연속적으로 보이는 양을 자연수라는 셀 수 있는 양으로 재해석하는 것
정보이론에서 양자화란, Analog(연속적)→Digital(이산적)인 값으로 바꾸어 근사하는 과정을 의미한다.
2. Model Quantization ?
딥러닝 모델에서의 가중치값은 float32(32bits 부동소수점)값을 갖는다.
이때, 가중치가 8bit로 양자화 된다면, 정확도는 덜 떨어지게 되는데, 배포 시 이 성능하락을 눈치채지 못할 수 있다.
(즉, 결과값에서 가중치의 정밀도는 성능에 미치는 영향이 적음을 의미)

위의 내용을 고려해본다면, 딥러닝에서 모델크기가 결정적일 때, 추론속도를 유의미하게 줄일 수 있다.
(가중치는 8bit로 저장, inference연산 시 float32로 수행)
또한 layer에 따라 모든 구성요소의 양자화 크기를 달리해 동작할 수 있다.(32bits, 16bits, 8bits)
3. 장점
Image에서 기본으로 갖는 Noise들을 줄여 robust한 특성을 갖게 할 수 있다.
inference연산 시 중복정보를 제거할 수 있다.

 

 


MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications (2017)

 

MobileNet은 객체검출, 얼굴속성, 미세분류, 표식인식 등과 같은 다양한 application program에 유용한 모델이다.

MobileNet은 기본적인 convolution kernel을 깊이방향(depth-wise)과 포인트방향(point-wise) convolution으로 대체하여 모델의 크기와 연산량을 줄인다.

 

 

모델 선택에 영향을 주는 매개변수
곱셈과 덧셈의 개수: 정확도와 다중덧셈사이의 절충값은 아래와 같다.

② 모델의 매개변수개수: 정확도와 매개변수의 수는 trade-off관계로 아래와 같다.

 

 

 


배포(Deploy)


클라우드에서 모델 배포하기

여러 application program에서 사용할 모델들은 cloud를 통해 배포하는데, 이를 위해 주요 cloud 서비스에 대해 알아보자.

 

AWS (Amazon Web Services)

AWS는 tensorflow기반 모델의 개발 및 배포를 지원한다.
https://aws.amazon.com/ko/ 에서 AWS 가입후 AMI(Amazon Machine Images)중 하나를 선택한다.
AMI는 필수 SW가 모두 설치된 컴퓨터의 이미지이기에 패키지 설치에 대한 걱정X

AWS는 딥러닝모델을 쉽게 훈련하고 배포하도록 DLAMI를 제공하는데, 여기서 여러 옵션을 선택할 수 있다.
source activate tensorflow_p36

 

위의 코드는 CUDA8에서 python3과 keras2로 tensorflow를 활성화 하는 코드이다.

이제 가상머신(VM)을 아래의 단계에 걸쳐 시작할 수 있다.

1. aws.amazon.com으로 이동 후 Amazon계정으로 로그인.
2. 로그인 페이지에서 Launch a virtual machine을 선택.
3. 선택후 나오는 윈도우에서 Get Started를 클릭, EC2 Instance를 선택.
4. EC2 Instance의 이름을 지정. (ex. tensorflow)
5. OS type을 지정. (ex. ubuntu, windows, ...)
6. Instance type을 선택 (인스턴스 타입은 다양한 RAM크기와 CPU 구성방법을 의미; t2.micro 선택추천)
7. 로그인에 사용되는 개인정보 PEM파일 생성.
8. 일정시간이 지나 인스턴스생성 완료상태가 표시되면 Proceed to EC2 console버튼을 클릭.
9. 이제 인스턴스가 생성되었으므로 Connect버튼을 클릭.
10. 인스턴스를 가상시스템의 명령프롬프트에 연결, 이때 이전단계에서 다운받은 PEM파일이 필요. 

11. 작업이 끝나면 Action ▶️ Instance State ▶️ Terminate를 클릭해 인스턴스를 완료, 시작.

 

 

 

 

GCP (Google Cloud Platform)
GCP는 AWS와 비슷하게 간단한 가상머신을 사용해 모델을 아래의 단계를 거쳐 training할 수 있다.

1. https://cloud.google.com/ 을 사용해 gmail로 플랫폼에 로그인
2. GO TO CONSOLE 버튼을 클릭해 콘솔로 이동
3. 오른쪽 상단메뉴()에서 Compute Engine ▶️ VM Instance를 클릭해 VM생성페이지로 이동
4. CREATE INSTANCE 버튼을 클릭해 필요 인스턴스를 생성
5. 이후 설정메뉴를 사용해 인스턴스유형을 선택할 수 있다.
  (Machine Type은 필요한 RAM과 CPU를 의미하며 사용자가 정의할 수 있고, 빠른 훈련을 위해 GPU를 선택할 수도 있다.)
6. Instance가 생성되면, 인스턴스에 대한 SSH🔽을 클릭, Open in browser window 옵션을 선택한다.
이를통해 브라우저로 콘솔을 열 수 있고 셸을 통해 tensorflow를 설치하고 모델훈련 및 배포가 가능하다.


GCP에서는 tensorflow를 사용하는 동안 사용자에게 도움이 되는 클라우드 머신러닝엔진이 있다.
훈련 및 배포 인프라 구축을 위해 GCP의 3가지 구성요소를 사용할 수 있다.
① 이미지 전처리를 위한 Cloud DataFlow
② 모델 훈련 및 배포를 위한 Cloud Machine Learning Engine
③ traindata, code, 결과를 저장하는 Google Cloud Storage

 

 


장치에서 모델 배포하기

장치에 배포를 한다는 것은 장치에 딥러닝 모델이 존재하고 장치에서 추론이 이뤄질 수 있음을 시사한다.

이제, 다양한 모바일 플랫폼에 배포하는 방법을 알아보자.

 

Jetson TX2
NVIDIA에서 공급하는 내장장치로 가볍고 소형이어서 드론 등에 적합하다.
또한, tensorflow를 위한 runtime인 TensorRT가 사전에 설치되어 있고 
tensorflow설치 전, Jetson과 flash를 설치하고 Ubuntu, CUDA, cuDNN을 설치할 수 있다.

https://github.com/jetsonhacks/installTensorFlowTX2 를 복사하고 명령프롬프트에서 아래 명령을 수행, Jetson TX2에 tensorflow를 설치한다.

1. 필수구성요소 설치
./installPrerequisites.sh


2. 아래 코드를 사용해 tensorflow를 복제
./cloneTensorFlow.sh


3. 아래 코드를 사용해 필요환경변수 설정
./setTensorFlowEV.sh


4. 아래 코드를 사용해 tensorflow를 build
./buildTensorFlow.sh


5. 아래 코드를 사용해 package를 wheel파일로 처리
./packageTensorFlow.sh


6. 아래 코드를 사용해 tensorflow를 설치
pip install $ HOME / tensorflow-1.0.1-cp27-cp27mu-linux_aarch64.whl

 

 

 

Android
모든 안드로이드앱에서 tensorflow를 사용할 수 있다.
build세부정보: https://www.tensorflow.org/lite/android/lite_build?hl=en
안드로이드에 대한 공식예제: https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android

안드로이드 기기에서 tensorflow를 구현하는 단계는 안드로이드 프로그래밍경험이 있다 가정할 때, 아래와 같다.
1. tensorflow모델을 .pb파일로 내보낸다.
2. .so.jar 바이너리 파일을 build한다.
3. 라이브러리를 불러올 수 있도록 gradle 파일을 편집한다.
4. 안드로이드 앱을 불러와 실행한다.

 

 

 

iOS
애플은 CoreML 프레임워크를 사용해 iPhone Application program에서 사용하는 머신러닝을 통합하여 application program에 직접 통합가능한 표준모델목록을 제공한다.

tensorflow를 사용해 맞춤식 딥러닝모델을 훈련, 이를 아이폰에서 사용할 수 있으며 custom모델을 배치하기 위해 tensorflow를 CoreML 프레임워크 모델로 변환해야 한다.
구글이 TF→CoreML에 대해 릴리즈한 내용: https://github.com/tf-coreml/tf-coreml

TFcoreML은 아래 코드를 사용해 설치할 수 있다
pip install -U tfcoreml


또한, 아래 코드를 사용해 모델을 내보내서 아이폰에서 예측하기 위해 사용할 수 있다.
import tfcoreml as tf_converter

tf_converter.convert(tf_model_path='tf_model_path.pb',
					 mlmodel_path='mlmodel_path.mlmodel',
                     output_feature_names=['softmax:0'].
                     input_name_shape_dict={'input:0' : [1, 227, 227, 3]}
					)

 

 

 

 

'Gain Study' 카테고리의 다른 글

[Gain Study]. Hello, Pytorch!  (0) 2023.08.22

📌 배열 랜더링하기

📌 useRef로 컴포넌트 내부 변수 생성하기

📌 배열에 항목 추가하기

📌 배열에 항목 제거하기

📌 배열에 항목 수정하기

 

 

 


🤔 배열 랜더링하기.
가령 아래와 같은 배열이 있을 때, 
const users = [
  { id: 1,
    username: 'velopert',
    email: 'public.velopert@gmail.com' },
  { id: 2,
    username: 'tester',
    email: 'tester@example.com' },
  { id: 3,
    username: 'liz',
    email: 'liz@example.com' }
];

이 내용을 컴포넌트로 랜더링 하고, 컴포넌트를 재사용 할 수 있게 한다면?
UserList.js

import React from 'react';

function User({ user }) {
  return (
    <div>
      <b>{user.username}</b> <span>({user.email})</span>
    </div>
  );
}

function UserList() {
  const users = [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ];

  return (
    <div>
      <User user={users[0]} />
      <User user={users[1]} />
      <User user={users[2]} />
    </div>
  );
}

export default UserList;





🤔 만약, 동적인 배열을 랜더링 해야한다면? → map() 을 사용!!
map() 함수는 배열안에 있는 각 원소를 변환하여 새로운 배열을 만든다.
react에서 동적인 배열을 랜더링해야 할 때는 이 함수를 사용하여 일반 데이터 배열을 react element로 이루어진 배열로 변환해주면 된다.

다만, react에서 배열을 랜더링할 때는 key라는 props를 설정해야 한다.
UserList.js

import React from 'react';

function User({ user }) {
  return (
    <div>
      <b>{user.username}</b> <span>({user.email})</span>
    </div>
  );
}

function UserList() {
  const users = [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ];

  return (
    <div>
      {users.map(user => (
        <User user={user} key={user.id} />
      ))}
    </div>
  );
}

export default UserList;


만약 배열 안의 원소가 가지고 있는 고유한 값이 없다면 
map() 함수를 사용 할 때 설정하는 콜백함수의 두번째 파라미터 index  key 로 사용하면 된다.

Ex)

<div>
  {users.map((user, index) => (
    <User user={user} key={index} />
  ))}
</div>

 

 

 

 


cf. useRef (https://chan4im.tistory.com/190)

 

<frontend> {14} </React_part 1>

🤔 React 라이브러리...? JS를 사용해 HTML로 구성된 UI를 제어했다면, DOM 변형을 위해 아래와 같은 과정을 따른다. 1. Selector API를 사용해 특정 DOM을 선택 2. 특정 이벤트가 발생 시, 변화를 주도록 설

chan4im.tistory.com

🤔 useRef로 컴포넌트 안의 변수 만들기
useRef의 Hook은 2가지 용도가 있다.
1. DOM 선택 용도
2. 컴포넌트 안에서 조회 및 수정할 수 있는 변수를 관리하는 용도

관리를 위한 변수
∙ setTimeout, setInterval을 통해 만들어진 id
∙ 외부 라이브러리를 사용해 생성된 인스턴스
∙ scroll 위치

Ex) UserList에서 컴포넌트 내부에 배열을 직접선언하는 대신, App에서 배열을 선언, UserList에게 props로 전달하는 예제
UserList.js

import React from 'react';

function User({ user }) {
  return (
    <div>
      <b>{user.username}</b> <span>({user.email})</span>
    </div>
  );
}

function UserList({ users }) {
  return (
    <div>
      {users.map(user => (
        <User user={user} key={user.id} />
      ))}
    </div>
  );
}

export default UserList;
App.js

import React, { useRef } from 'react';
import UserList from './UserList';

function App() {
  const users = [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ];

  const nextId = useRef(4);
  const onCreate = () => {
    // 나중에 구현 할 배열에 항목 추가하는 로직
    // ...

    nextId.current += 1;
  };
  return <UserList users={users} />;
}

export default App;

App 에서 useRef() 를 사용하여 nextId 라는 변수를 생성하며,

useRef() 를 사용 할 때 파라미터를 넣어주면, 이 값이 .current 값의 기본값이 된다.

그리고 이 값을 수정 할때에는 .current 값을 수정하면 되고 조회 할 때에는 .current 를 조회하면 된다.

 

 

 

 

 

 

 

 


먼저 input 2개와 button 1개로 이루어진 CreateUser.js라는 컴포넌트를 src디렉토리에 추가

import React from 'react';

function CreateUser({ username, email, onChange, onCreate }) {
  return (
    <div>
      <input
        name="username"
        placeholder="계정명"
        onChange={onChange}
        value={username}
      />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
      />
      <button onClick={onCreate}>등록</button>
    </div>
  );
}

export default CreateUser;
🤔 배열에 항목 추가하기
위의 CreateUser에서 컴포넌트의 상태관리를 하지 않고
부모 컴포넌트인 App에서 상태관리를 진행, input값 및 이벤트 등록 함수들은 props로 넘겨받아 사용해보자.

이 컴포넌트를 App에서 UserList위에 랜더링 해보자.
App.js

import React, { useRef } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function App() {
  const users = [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ];

  const nextId = useRef(4);
  const onCreate = () => {
    // 나중에 구현 할 배열에 항목 추가하는 로직
    // ...

    nextId.current += 1;
  };
  return (
    <>
      <CreateUser />
      <UserList users={users} />
    </>
  );
}

export default App;

CreateUser 컴포넌트에게 필요한 props를 App에서 준비,
그 후, users에도 useState를 사용해 컴포넌트의 상태로서 관리.
import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ]);

  const nextId = useRef(4);
  const onCreate = () => {
    // 나중에 구현 할 배열에 항목 추가하는 로직
    // ...

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} />
    </>
  );
}

export default App;


이제 배열에 변화를 줘야하는데, 이때 객체와 마찬가지로 불변성을 지켜줘야 한다.
따라서 배열의 push, splice, sort 등의 함수는 사용X
if, 사용해야할 때, 기존 배열을 한번 복사한 후 사용해야함.

📌 불변성을 지키면서 배열에 새 항목을 추가하는 방법
1. spread 연산자 사용
2. concat 함수 사용


- spread 연산자 사용(App.js)
import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ]);

  const nextId = useRef(4);
  const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers([...users, user]);

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} />
    </>
  );
}

export default App;

- concat 함수 사용(App.js)
import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ]);

  const nextId = useRef(4);
  const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} />
    </>
  );
}

export default App;

 

 

 

 

 

 


우선, UserList에 각 User 컴포넌트에 대해 삭제 버튼을 랜더링.

UserList.js

import React from 'react';

function User({ user, onRemove }) {
  return (
    <div>
      <b>{user.username}</b> <span>({user.email})</span>
      <button onClick={() => onRemove(user.id)}>삭제</button>
    </div>
  );
}

function UserList({ users, onRemove }) {
  return (
    <div>
      {users.map(user => (
        <User user={user} key={user.id} onRemove={onRemove} />
      ))}
    </div>
  );
}

export default UserList;
🤔 배열에 항목 제거하기
이때, User 컴포넌트의 삭제버튼이 클릭될 때, user.id값을 앞으로 props로 받아올 onRemove 함수의 파라미터로 넣어 호출해야
한다. (onRemove함수는 "id가 ~인 객체를 삭제하라"는 역할)
이때, onRemove함수는 UserList에서도 전달받고, 이를 그대로 User 컴포넌트에게 전달한다.


이제 onRemove를 구현해보자.
배열에 있는 항목제거 시, 추가할 때와 마찬가지로 불변성을 지켜가며 업데이트 해줘야 한다.
이때, filter라는 배열 내장 함수를 사용하는 것을 추천!!

App.js
import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ]);

  const nextId = useRef(4);
  const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };

  const onRemove = id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users.filter(user => user.id !== id));
  };
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} />
    </>
  );
}

export default App;

 

 

 

 

 

 


먼저 App 컴포넌트의 users 배열 안의 객체 안에 active라는 속성을 추가하자.

 

App.js 

import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]);

  const nextId = useRef(4);
  const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };

  const onRemove = id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users.filter(user => user.id !== id));
  };

  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
    </>
  );
}

export default App;​
🤔 배열에 항목 수정하기
User 컴포넌트에 계정명 클릭 시, 색상이 초록색으로 바뀌고 다시 누르면 검정색으로 바뀌도록 하는 구현

이제, User 컴포넌트에서 active 값에 따라 폰트색이 바뀌도록 해보자.
cf. cursor 필드를 설정해 마우스를 올렸을 때, 손가락 모양으로 바뀌도록 하자.
UserList.js

import React from 'react';

function User({ user, onRemove }) {
  return (
    <div>
      <b
        style={{
          cursor: 'pointer',
          color: user.active ? 'green' : 'black'
        }}
      >
        {user.username}
      </b>

      <span>({user.email})</span>
      <button onClick={() => onRemove(user.id)}>삭제</button>
    </div>
  );
}

function UserList({ users, onRemove }) {
  return (
    <div>
      {users.map(user => (
        <User user={user} key={user.id} onRemove={onRemove} />
      ))}
    </div>
  );
}

export default UserList;

 


이제 App.js 에서 onToggle 이라는 함수를 구현, 
배열의 불변성을 유지하면서 배열을 업데이트 할 때에도 map 함수를 사용 할 수 있다.

id 값을 비교해서 id 가 다르다면 그대로 두고, 같다면 active 값을 반전시키도록 구현을 진행,

onToggle 함수를 만들어서 UserList 컴포넌트에게 전달한다.

App.js

import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]);

  const nextId = useRef(4);
  const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };

  const onRemove = id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users.filter(user => user.id !== id));
  };
  const onToggle = id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  };
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
    </>
  );
}

export default App;




그 다음에는 UserList 컴포넌트에서 onToggle를 받아와서 User 에게 전달해주고, onRemove 를 구현했었던것처럼 onToggle  id 를 넣어서 호출하자.
UserList.js

import React from 'react';

function User({ user, onRemove, onToggle }) {
  return (
    <div>
      <b
        style={{
          cursor: 'pointer',
          color: user.active ? 'green' : 'black'
        }}
        onClick={() => onToggle(user.id)}
      >
        {user.username}
      </b>
      &nbsp;
      <span>({user.email})</span>
      <button onClick={() => onRemove(user.id)}>삭제</button>
    </div>
  );
}

function UserList({ users, onRemove, onToggle }) {
  return (
    <div>
      {users.map(user => (
        <User
          user={user}
          key={user.id}
          onRemove={onRemove}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
}

export default UserList;

 

https://chan4im.tistory.com/190

 

<frontend> {14} </React_part 1>

🤔 React 라이브러리...? JS를 사용해 HTML로 구성된 UI를 제어했다면, DOM 변형을 위해 아래와 같은 과정을 따른다. 1. Selector API를 사용해 특정 DOM을 선택 2. 특정 이벤트가 발생 시, 변화를 주도록 설

chan4im.tistory.com

앞선 과정에서 react 컴포넌트를 만들 때, 동적인 부분은 하나도 없었다.

즉, 값이 바뀌는 일이 없었지만, 이번에는 사용자의 interaction에 따른 변화가 필요할 때, 어떻게 구현할 수 있는지 알아볼 필요가 있다.

 

Ex. 버튼을 누르면 숫자가 바뀌는 Counter 컴포넌트를 생성해보자!

Counter.js

import React from 'react';

function Counter() {
  return (
    <div>
      <h1>0</h1>
      <button>+1</button>
      <button>-1</button>
    </div>
  );
}

export default Counter;
App.js

import React from 'react';
import Counter from './Counter';

function App() {
  return (
    <Counter />
  );
}

export default App;

 

📌 이제, Counter에서 버튼이 클릭되는 이벤트가 발생한다면?

→ 특정 함수가 호출되도록 설정해보자.

여기서 onIncrease  onDecrease 는 화살표 함수를 사용해 button의 onClick 으로 함수를 연결해주었다.

즉, react에서 element에 이벤트 설정시, 다음의 형태로 설정한다.

on이벤트이름 = {실행하고싶은 함수}
Counter.js

import React from 'react';

function Counter() {
  const onIncrease = () => {
    console.log('+1')
  }
  const onDecrease = () => {
    console.log('-1');
  }
  return (
    <div>
      <h1>0</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;

cf. =>함수 참고

좌측에는 함수의 파라미터, 화살표의 우측에는 코드 블록이 들어온다.

(https://learnjs.vlpt.us/basics/05-function.html#%ED%99%94%EC%82%B4%ED%91%9C-%ED%95%A8%EC%88%98)

 

05. 함수 · GitBook

함수는, 특정 코드를 하나의 명령으로 실행 할 수 있게 해주는 기능입니다. 예를 들어서, 우리가 특정 값들의 합을 구하고 싶을 때는 다음과 같이 코드를 작성합니다. 한번, 이 작업을 함수로 만

learnjs.vlpt.us

 

 

 

🤔 동적인 값에 대한 컴포넌트 상태관리, useState..?

Ex. 아래 코드는 react 패키지에서 useState라는 함수를 불러온다.
이때, Setter함수는 param에서 전달받은 값을 최신값으로 설정한다.
Counter.js

import React, { useState } from 'react';

function Counter() {
  const [number, setNumber] = useState(0);

  const onIncrease = () => {
    setNumber(number + 1);
  }

  const onDecrease = () => {
    setNumber(number - 1);
  }

  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;

 

 

 

 

 

 


 

InputSample.js

import React from 'react';

function InputSample() {
  return (
    <div>
      <input />
      <button>초기화</button>
      <div>
        <b>값: </b>
      </div>
    </div>
  );
}

export default InputSample;​
App.js

import React from 'react';
import InputSample from './InputSample';

function App() {
  return (
    <InputSample />
  );
}

export default App;​


여기에 대해, input에 입력하는 값이 아래에 나타나고, 초기화버튼을 누르면 input 값이 비워지도록 구현해보자.

useState를 사용, input의 onChange 이벤트를 사용할 때,
이벤트에 등록하는 함수에서는 이벤트 객체 e 를 파라미터로 받아와서 사용 할 수 있고,
이 객체의 
e.target 은 이벤트가 발생한 DOM 인 input DOM 을 가르키게 된다.
이 DOM 의 
value 값, 즉 e.target.value 를 조회하면 현재 input 에 입력한 값이 무엇인지 알 수 있으며, 이 값을 useState를 통해 관리해주면 된다.

InputSample.js

import React, { useState } from 'react';

function InputSample() {
  const [text, setText] = useState('');

  const onChange = (e) => {
    setText(e.target.value);
  };

  const onReset = () => {
    setText('');
  };

  return (
    <div>
      <input onChange={onChange} value={text}  />
      <button onClick={onReset}>초기화</button>
      <div>
        <b>값: {text}</b>
      </div>
    </div>
  );
}

export default InputSample;

 

 

 

 

 

 

 


 

🤔 useRef로 특정 DOM 선택하기
JS에서 특정 DOM을 선택해야할 때, getElementId, querySelector 같은 DOM Selector함수를 사용해 DOM을 선택한다.

React에서도 가끔 DOM을 직접 선택하는 상황이 발생하는데, 이때 ref라는 것을 사용한다.
함수형 컴포넌트에서 ref사용 시, useRef라는 Hook함수를 사용한다.


Ex) 초기화 버튼 클릭 시, 이름 input에 포커스가 잡히는 기능 구현
InputSample.js


import React, { useState, useRef } from 'react';

function InputSample() {
  const [inputs, setInputs] = useState({
    name: '',
    nickname: ''
  });
  const nameInput = useRef();

  const { name, nickname } = inputs; // 비구조화 할당을 통해 값 추출

  const onChange = e => {
    const { value, name } = e.target; // 우선 e.target 에서 name 과 value 를 추출
    setInputs({
      ...inputs, // 기존의 input 객체를 복사한 뒤
      [name]: value // name 키를 가진 값을 value 로 설정
    });
  };

  const onReset = () => {
    setInputs({
      name: '',
      nickname: ''
    });
    nameInput.current.focus();
  };

  return (
    <div>
      <input
        name="name"
        placeholder="이름"
        onChange={onChange}
        value={name}
        ref={nameInput}
      />
      <input
        name="nickname"
        placeholder="닉네임"
        onChange={onChange}
        value={nickname}
      />
      <button onClick={onReset}>초기화</button>
      <div>
        <b>값: </b>
        {name} ({nickname})
      </div>
    </div>
  );
}

export default InputSample;

useRef() 를 사용하여 Ref 객체를 만들고, 이 객체를 우리가 선택하고 싶은 DOM 에 ref 값으로 설정해주면, Ref 객체의 .current 값은 우리가 원하는 DOM 을 가르키게 된다.

🤔 React 라이브러리...?

 


JS를 사용해 HTML로 구성된 UI를 제어했다면, DOM 변형을 위해 아래와 같은 과정을 따른다.
1. Selector API를 사용해 특정 DOM을 선택
2. 특정 이벤트가 발생 시, 변화를 주도록 설정

다만, 웹의 규모가 커지게 되면, DOM을 직접 건드릴 때 코드가 난잡해지기 쉽다.
처리해야할 이벤트, 관리해야할 상태값, DOM 등이 다양해지기 때문.
이로 인해 업데이트 규칙도 많이 복잡해지게된다.

React는 어떤 상태가 바뀌었을 때, 그 상태에 따라 DOM을 어떻게 업데이트 할 지 규칙을 정하는 방식이 아닌, 아예 다 날려버리고 처음부터 새로 만드는, 가상의 DOM인 Virtual DOM방식을 사용한다.

Ex.
import React from 'react';
를 통해 react 컴포넌트를 불러온다.
react 컴포넌트는 XML형식의 값을 반환하는데, 이를 JSX라 부른다.

export default Hello;
이는 Hello라는 컴포넌트를 내보내겠다는 의미로 다른 컴포넌트에서 불러와 사용할 수 있다.
ex)
import Hello from './Hello';

import React from 'react';

function Hello() {
    return (
            <div>
                Hello World
            </div>
        );
}

export default Hello;

 

 

 

 

 

 

 


🤔 JSX...?

JSX는 react의 생김새를 정의할 때 사용하는 문법으로 HTML같이 생긴 JS이다.

return <div>안녕</div>;
react 컴포넌트 파일에서 XML형태로 코드를 작성하면 babel이 JSX를 JS로 변환한다.
babel: JS의 문법을 확장해주는 도구

ex)
import React from 'react';
import Hello from './Hello';

function App() {
  return (
    <>
      <Hello />
      <div>안녕히계세요</div>
    </>
  );
}

export default App;


📌 JSX에서 태그의 style, CSS의 class를 설정하는 방법
1. 인라인 style은 객체형태로 작성
2. HTML에서 -로 구분되는 단어는 camelCase형태로 작성
3. CSS의 클래스는 className= 으로 설정

cf. 주석은 {/**/} 나 // 을 사용함.
를 사용해 HTML로 구성된 UI를 제어했다면, DOM 변형을 위해 아래와 같은 과정을 따른다.

1. Selector API를 사

 

 

 

 

 


📌 props를 통해 컴포넌트에 값 전달하기
- properties의 줄임말로 컴포넌트에 값을 전달하기 위해 사용.

Ex) App 컴포넌트에서 Hello컴포넌트의 name이라는 값을 전달하고 싶다면?

∙Hello.js 컴포넌트
import React from 'react';

function Hello(props) {
  return <div>안녕하세요 {props.name}</div>
}

export default Hello;

 

∙App.js 컴포넌트
import React from 'react';
import Hello from './Hello';

function App() {
  return (
    <Hello name="react" />
  );
}

export default App;

 

cf) 여러개의 props를 전달하고자 한다면?

∙App.js
import React from 'react';
import Hello from './Hello';

function App() {
  return (
    <Hello name="react" color="red"/>
  );
}

export default App;​


∙Hello.js

import React from 'react';

function Hello({ color, name }) {
  return <div style={{ color }}>안녕하세요 {name}</div>
}

export default Hello;

 

defaultProps 로 기본값 설정

컴포넌트에 props 를 지정하지 않았을 때 기본적으로 사용 할 값을 설정하고 싶다면 컴포넌트에 defaultProps 라는 값을 설정하면 된다.

import React from 'react';

function Hello({ color, name }) {
  return <div style={{ color }}>안녕하세요 {name}</div>
}

Hello.defaultProps = {
  name: '이름없음'
}

export default Hello;

 

Javascript에서 객체란?
웹, 프로그램에서 인식가능한 모든 대상으로 JS에서 사용하는 객체는 다음과 같다.

∙내장 객체 : 웹프로그래밍 시 자주 사용하는 요소는 JS에 미리 객체로 정의되어 있다.

∙브라우저 관련 객체 : 웹 브라우저에서 사용하는 정보도 객체이다.

∙문서객체모델(DOM) : 웹문서 내부 삽입된 링크는 물론, 웹문서 그 자체도 객체이다.

 

 

 

 

1.  기본적인 객체의 인스턴스 만들기

📌 객체의 인스턴스 만들기

new 객체명

ex)
let current = new Date();
document.write("현재 시각은 " + now.toLocaleString());

 

 

 

 


2.  Javascript의 내장 객체

 


🤔 Array 객체

◼︎ Array 객체로 배열 만들기
let num = new Array(4);	// 배열크기가 4인 num배열 선언


let arr = ['a', 'b', 'c', 'd'];

for (let i = 0; i < arr.length; i++){
	document.write("<p>" + arr[i] + "</p>");
}


◼︎ Array 객체 메서드
∙ concat
∙ every
∙ filter
∙ forEach
∙ indexOf : 주어진 값과 일치하는 배열요소의 첫 인덱스를 찾는다.
∙ join : 배열 요소를 문자열로 합치며 구분자를 지정 가능
∙ push : 맨끝에 요소 추가
∙ unshift : 시작부분에 요소 추가

∙ pop : 맨끝 요소 반환
∙ shift : 첫 요소 반환
∙ splice : 배열에 요소 추가/삭제
∙ slice : 배열에 특정부분 잘라냄
∙ reverse 
∙ sort
∙ toString : 배열에서 지정한 부분을 문자열로 반환, 요소는 (,)로 구분


https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array
 

Array - JavaScript | MDN

JavaScript Array 클래스는 리스트 형태의 고수준 객체인 배열을 생성할 때 사용하는 전역 객체입니다.

developer.mozilla.org

 

 


🤔 Date 객체

◼︎ Date 객체를 사용해 날짜와 시간을 지정.
- 이때, JS가 인식할 수 있는 형식으로 날짜와 시간을 써야한다.

    1. YYYY-MM-DD 형식
    2. YYYY-MM-DDTHH 형식
    3. MM/DD/YYYY 형식
new Date("2023-07-05")
new Date("2023-07-05T20:41:13")
new Date("02/28/2025")



◼︎ Date 객체 메서드

 

 


🤔 Math 객체
◼︎ 사용 방식
Math.프로퍼티명
Math.메서드명

ex)
Math.random()


◼︎ Math 객체 property 및 메서드

 

 

 

 

 

 

 

 

 

 

 


3.  브라우저 관련 객체

웹 브라우저 창에 문서가 표시되면 브라우저는 HTML소스를 한줄씩 읽으며 화면에 내용을 표시, 관련 객체를 생성한다.

 

 

 

 

 

 

 

 

 

 

 

 

 


4.  문서객체모델 (DOM)

 

📌 DOM 트리

 

📌 DOM 구성 기본원칙

  • 모든 HTML 태그는 요소(element)노드이다.
  • HTML 태그에서 사용하는 텍스트 내용은 자식노드인 텍스트 노드이다.
  • HTML 태그의 속성은 자식노드인 속성노드이다.
  • 주석은 주석(comment)노드이다.

 

 

🤔 DOM 요소에 접근하고 속성 가져오기

📌 DOM에 접근하기
// id 선택자로 접근하는 getElementById() 메서드
요소명.getElementById("id명")

// class값으로 접근하는 getElementByClassName() 메서드
요소명.getElementByClassName("class명")

// 태그 이름으로 접근하는 getElementByTagName() 메서드
요소명.getElementByTagName("태그명")

// 다양한 방법으로 접근하는 querySelector(), querySelectorAll() 메서드
노드.querySelector(선택자)
노드.querySelectorAll(선택자 | 태그)


📌 웹 요소 내용을 수정하는 innerText, innerHTML 프로퍼티
요소명.innerText = 내용
요소명.innerHTML = 내용


📌 속성을 가져오거나 수정하는 getAttribute(), setAttribute() 메서드
getAttribute("속성명")
setAttribute("속성명", "값")

 

 

 

🤔 DOM에서 이벤트 처리하기

📌 DOM 요소에 함수 직접 연결

📌 함수 이름을 사용해 연결

📌 DOM의 event 객체 알아보기

📌 addEventListener() 메서드 사용하기

📌 CSS 속성에 접근하기

 

 

 

🤔 DOM에서 노드 추가∙삭제하기

📌 노드 리스트란

📌 텍스트 노드를 사용하는 새로운 요소 추가

📌 속성값이 있는 새로운 요소 추가

📌 노드 삭제하기

📌 CSS 속성에 접근하기

 

 

이후 추가작성

(Typescript로 부트캠프가 방향을 선회해버려서...)

 

📌 변수 선언하기
var age = 23;​

 

cf. 만약 var 명령어를 빼게 된다면, 이는 전역변수를 의미한다.

🤔 HTML 파일에 코드 입력하기
<body></body> 태그 사이에 소스코드 입력
<!DOCTYPE html>
<html lang="kor">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
            var currentYear = 2023;
            var birth, age;

            birth = prompt("태어난 년도를 입ㅕ하세요. (YYYY)", "");
            age = currentYear - birth + 1;
            document.write(currentYear + "년 현재<br>");
            document.write(birth + "년 생의 현재 나이는" + age + "세 입니다.");
    </script>
</body>
</html>

 


 

📌 Javascript의 자료형

cf. undefined: 자료형이 정의되지 않을 때의 데이터 상태 (즉, 변수에 값이 할당되지 않았음을 의미)
cf. null: 데이터의 값이 유효하지 않은 상태 (즉, 할당은 되었지만 할당된 값이 유효하지 않음을 의미)


📌 Javascript의 조건문
if (조건) {
}
else {
}



📌 Javascript의 반복문
for(let i= 1; i < 6; i++) {
	sum += i;
}

while(let i < 6) {
	sum += i;
    i++;
}




📌 함수의 사용 in Javascript
function 함수명() {
	명령
}

ex) 
<script>
    function AddNum() {
        var n1=2;
        var n2=3;
        var sum = n1 + n2;
        alert("결과:" + sum);
    }

    AddNum();
</script>

<script>
    function AddNum(num1, num2) {
        var sum = num1 + num2;
        return sum;
    }
	var res = AddNum(3, 5);
    document.write(res);
</script>

 

 

🤔 let과 const
var
를변수 선언시 보통 사용하며, var를 사용하지 않으면 자동으로 전역변수가 되어 버린다.
이를 방지하고자 let, const를 사용한다.

추가적으로 var, let, const의 가장 큰 차이는 scope 범위이다.
 ∙var: 함수영역의 스코프
 ∙let과 const: 블록영역{ }의 스코프
    - 보통 let 은 가변변수, const는 상수변수에 사용된다.


📌 Javascript 변수사용 Tip!
1. var 변수는 함수 시작부분에서 선언!
2. for문에서 counting변수는 var 예약어 사용X (let 사용)
3. 보통 var보다 let을 주로 사용하는 것이 좋다.
4. 전역변수는 최소한으로 사용.

 

 


 

 

🤔 이벤트와 이벤트 처리기


📌 이벤트 처리기
<태그 on이벤트 = "함수명">

ex)
<body>
	<ul>
    	<li><a href="#" onclick="changeBg('green')">Green</a></li>
        <li><a href="#" onclick="alert('green')">Green</a></li>
    </ul>
</body>
📌 외부 스크립트 파일로 연결해 자바스크립트 작성하기
<script src="외부 스크립트 파일 경로"></script>

ex)
<body>
	<h1 id="heading">자바스크립트</h1>
    <p id="text">위 텍스트를 클릭하세요</p>
    
    <script src="js/color.js"></script>
</body>​

🤔 JS 용어 및 기본 입출력 방법

◼︎ 식과 문 (expression. &. statement)
연산식, 문자열, 숫자도 식이다.
inch * 2.54	  // 연산식
"안녕하세요?";	// 문자열
5	         // 숫자


◼︎ 알림 창 출력하기 - alert(메세지)
◼︎ 확인 창 출력하기 - confirm(메세지)
◼︎ 프롬프트 창에서 입력받기 - prompt(메세지)
var reply = confirm("정말 창을 새로고침 하시겠습니까?");

var name = prompt("이름을 입력하세요.", "홍길동");


◼︎ 웹 브라우저 화면에 출력을 담당하는 document.write()문

<script>
	var name = prompt("이름을 입력하세요.");
    document.write("<b><big>" + name + "</big></big>님, 환영합니다.");
</script>



◼︎ 콘솔 창에 출력하는 console.log()문

<body>
	<h1>어서오세요</h1>
    
    <script>
    	var name = prompt("이름을 입력하세요.");
        console.log(name + "님, 환영합니다.");
    </script>
</body>

 

+ Recent posts