for textmining

Softmax-with-Loss 계층

|

이번 글에서는 소프트맥스 함수와 크로스엔트로피 손실함수가 합쳐진 ‘Softmax-with-Loss’ 계층에 대해 살펴보도록 하겠습니다. 이 글은 위키피디아와 ‘밑바닥부터 시작하는 딥러닝’, 그리고 이곳을 정리했음을 먼저 밝힙니다. 그럼 시작하겠습니다.

개요

다범주 분류문제를 풀기 위한 딥러닝 모델 말단엔 소프트맥스 함수가 적용됩니다. 소프트맥스 함수는 범주 수만큼의 차원을 갖는 입력벡터를 받아서 확률(요소의 합이 1)로 변환해 줍니다. 이후 손실 함수로는 크로스엔트로피(cross entropy)가 쓰이는데요. 크로스엔트로피는 소프트맥스 확률의 분포와 정답 분포와의 차이를 나타냅니다. 이를 기본으로 해서 손실(오차)을 최소화하는 방향으로 모델의 각 파라메터를 업데이트하는 과정이 바로 딥러닝 모델의 학습이 되겠습니다.

그런데 딥러닝 모델 학습시 손실에서 나오는 그래디언트를 계산하는 것이 제1관문이 됩니다. 그도 그럴 것이 체인룰(chain rule)에 의해 이 그래디언트에 각 계산 과정에서의 로컬 그래디언트가 끊임없이 곱해져 오차가 역전파(backpropagation)되기 때문입니다. 이렇게 손실(오차)에 대한 각 파라메터의 그래디언트를 구하게 되면 그래디언트 디센트(gradient descent) 기법으로 파라메터를 업데이트해 손실을 줄여 나가게 됩니다. 딥러닝 모델의 손실함수로 왜 크로스엔트로피가 쓰이는지에 대해선 이곳을, 그래디언트 디센트(gradient descent)와 관련해서는 이곳을, 오차 역전파와 관련해서는 이곳을 참고하시면 좋을 것 같습니다.

이번 글에서는 딥러닝 역전파의 첫 단추인 Softmax-with-Loss 계층을 살펴보도록 하겠습니다. 이 글에서는 별도 표시가 없는 한 스칼라를 기준으로 표기하였음을 먼저 밝힙니다.

순전파

분류해야 할 범주 수가 $n$이고 소프트맥스 함수의 $i$번째 입력값을 $a_i$, $i$번째 출력값을 $p_i$라고 할 때 소프트맥스 함수는 다음과 같이 정의됩니다. 소프트맥스 함수의 입력 및 출력벡터의 차원수는 $n$이 됩니다.

[{ p }{ i }=\frac { exp\left( { a }{ i } \right) }{ \sum { k }^{ }{ exp\left( { a }{ k } \right) } }]

딥러닝 모델의 손실(오차) $L$은 다음과 같이 크로스엔트로피로 정의됩니다. 스칼라 값입니다. 아래에서 $y_j$는 정답 벡터의 $j$번째 요소라는 뜻입니다. 예컨대 3범주 분류를 하는 문제에서 어떤 데이터의 정답이 첫번째 범주라면 $y=[1,0,0]$이 되고, $y_1=1$, 나머지 $y_2, y_3$은 0이 됩니다. $p_j$는 소프트맥스 함수의 $j$번째 출력값입니다.

[L=-\sum { j }^{ }{ { y }{ j }\log { { p }_{ j } } }]

미분 기초

역전파를 본격적으로 살펴보기에 앞서 몇 가지 미분 공식을 정리하고 넘어가겠습니다.

[\begin{align} y=exp\left( x \right) &\leftrightharpoons \frac { \partial y }{ \partial x } =exp\left( x \right) \ y=\log { x } &\leftrightharpoons \frac { \partial y }{ \partial x } =\frac { 1 }{ x } \ y=\frac { f\left( x \right) }{ g\left( x \right) }& \leftrightharpoons \frac { \partial y }{ \partial x } =\frac { f^{ ‘ }\left( x \right) g\left( x \right) -f\left( x \right) g^{ ‘ }\left( x \right) }{ { g\left( x \right) }^{ 2 } } \end{align}]

소프트맥스 함수의 그래디언트

소프트맥스 함수의 $i$번째 출력값 $p_i$, $j$번째 출력값 $p_j$에 대한 Softmax-with-Loss 계층의 $i$번째 입력값 $a_i$의 그래디언트는 각각 다음과 같습니다. 우선 $i=j$인 경우부터 살펴보겠습니다.

[\frac { \partial { p }{ i } }{ \partial { a }{ i } } =\frac { \partial \frac { exp\left( { a }{ i } \right) }{ \sum _{ k }^{ }{ exp\left( { a }{ k } \right) } } }{ \partial { a }_{ i } }]

$p_i$는 분수형 함수이므로 분자 $exp(a_i)$를 $f$, 분모 $Σ_kexp(a_k)$를 $g$로 보고 위 미분공식을 활용해 다시 적으면 다음과 같습니다. 여기에서 $Σ_kexp(a_k)$를 $a_i$에 대해 편미분한 결과는 $exp(a_i)$를 제외한 나머지 항은 상수 취급돼 소거되므로 $g’$은 $exp(a_i)$가 됩니다.

[\begin{align} \frac { \partial { p }_{ i } }{ \partial { a }_{ i } } &=\frac { exp\left( { a }_{ i } \right) \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } -exp\left( { a }_{ i } \right) exp\left( { a }_{ i } \right) }{ { \left( \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } \right) }^{ 2 } } \ &=\frac { exp\left( { a }_{ i } \right) \left[ \sum _{ k }^{ }{ \left{ exp\left( { a }_{ k } \right) \right} } -exp\left( { a }_{ i } \right) \right] }{ { \left( \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } \right) }^{ 2 } } \ &=\frac { exp\left( { a }_{ i } \right) }{ \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } } \frac { \sum _{ k }^{ }{ \left{ exp\left( { a }_{ k } \right) \right} } -exp\left( { a }_{ i } \right) }{ \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } } \ &=\frac { exp\left( { a }_{ i } \right) }{ \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } } \left( 1-\frac { exp\left( { a }_{ i } \right) }{ \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } } \right) \ \&={ p }_{ i }\left( 1-{ p }_{ i } \right) \end{align}]

다음은 $i≠j$인 경우입니다. $p_i$는 분수형 함수이므로 분자 $exp(a_i)$를 $f$, 분모 $Σ_kexp(a_k)$를 $g$로 보고 미분공식을 활용해 적으면 다음과 같습니다. 그런데 $exp(a_i)$를 $a_j$에 대해 편미분하면 0이 되므로 $f’g$ 역시 0이 됩니다. 아울러 여기에서 $Σ_kexp(a_k)$를 $a_j$에 대해 편미분한 결과는 $exp(a_j)$를 제외한 나머지 항은 상수 취급돼 소거되므로 $g’$은 $exp(a_j)$가 됩니다.

[\begin{align} \frac { \partial { p }_{ i } }{ \partial { a }_{ j } } &=\frac { 0-exp\left( { a }_{ i } \right) exp\left( { a }_{ j } \right) }{ { \left( \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } \right) }^{ 2 } } \ &=-\frac { exp\left( { a }_{ i } \right) }{ \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } } \frac { exp\left( { a }_{ j } \right) }{ \sum _{ k }^{ }{ exp\left( { a }_{ k } \right) } } \ \ &=-{ p }_{ i }{ p }_{ j } \end{align}]

역전파

손실에 대한 Softmax-with-Loss 계층의 $i$번째 입력값 $a_i$의 그래디언트는 다음과 같이 유도됩니다.

[\begin{align} \frac { \partial L }{ \partial { a }_{ i } } &=\frac { \partial \left( -\sum _{ j }^{ }{ { y }_{ j }\log { { p }_{ j } } } \right) }{ \partial { a }_{ i } } \ &=-\sum _{ j }^{ }{ { y }_{ j } } \frac { \partial \log { { p }_{ j } } }{ \partial { a }_{ i } } \ &=-\sum _{ j }^{ }{ { y }_{ j } } \frac { 1 }{ { p }_{ j } } \frac { \partial { p }_{ j } }{ \partial { a }_{ i } } \end{align}]

소프트맥스 함수의 그래디언트($\partial{p_j}/\partial{a_i}$)는 $i$와 $j$가 같을 때와 다를 때 각기 상이한 값이 도출되므로 위 식의 시그마 부분에서 $i$번째 입력값에 해당하는 요소를 분리해 두 개의 항으로 표현하면 다음과 같습니다. 여기에서 소프트맥스 확률의 합 $Σ_jy_j$는 1이 됩니다.

[\begin{align} -\sum _{ j }^{ }{ { y }_{ j } } \frac { 1 }{ { p }_{ j } } \frac { \partial { p }_{ j } }{ \partial { a }_{ i } } &=-\frac { { y }_{ i } }{ { p }_{ i } } { p }_{ i }\left( 1-{ p }_{ i } \right) -\sum _{ i\neq j }^{ }{ \frac { { y }_{ j } }{ { p }_{ j } } } \left( -{ p }_{ i }{ p }_{ j } \right) \ &=-{ y }_{ i }+{ y }_{ i }{ p }_{ i }+\sum _{ i\neq j }^{ }{ { y }_{ j }{ p }_{ i } } \ &=-{ y }_{ i }+\sum _{ j }^{ }{ { y }_{ j }{ p }_{ i } } \ &=-{ y }_{ i }+{ p }_{ i }\sum _{ j }^{ }{ { y }_{ j } } \ \&={ p }_{ i }-{ y }_{ i } \end{align}]

코드 구현

요컨대 Softmax-with-Loss 노드의 그래디언트를 구하려면 입력 벡터에 소프트맥스를 취한 뒤, 정답 레이블에 해당하는 요소값만 1을 빼주면 된다는 얘기입니다. 이를 파이썬 코드로 구현하면 아래와 같습니다.

import numpy as np
p = np.exp(a) / np.sum(np.exp(a)) # softmax 확률 계산
da = np.copy(p)
da[target] -= 1 # target=정답 인덱스

수식은 복잡하게 전개됐지만 그래디언트를 구하기가 매우 쉽고, 이렇게 구한 그래디언트 또한 0으로 죽는 일이 많지 않아서 소프트맥스+크로스 엔트로피가 많이들 쓰이는 것 같습니다.

Comment  Read more

한국어의 문장성분

|

이번 글에서는 한국어의 문장 성분에 대해 살펴보도록 하겠습니다. 이번 글은 고려대 정연주 선생님 강의와 ‘한국어문법총론1(구본관 외 지음, 집문당 펴냄)’을 정리했음을 먼저 밝힙니다. 그럼 시작하겠습니다.

문장성분

문장성분이란 한 문장을 구성하는 요소들을 문법적 기능에 따라 나눈 것을 가리킵니다. 여기서 문법적 기능이란 그 요소가 해당 문장 속에서 다른 요소와 어떤 (문법적) 관계를 가지면서 어떤 일을 하고 있는가를 나타냅니다. 예컨대 ‘주어’는 서술어가 나타내는 동작이나 상태의 주체가 되는 말입니다. ‘목적어’는 타동사가 쓰인 문장에서 동작의 대상이 되는 말입니다. ‘서술어’는 한 문장에서 주어의 움직임, 상태, 성질 따위를 서술하는 말입니다.

문장성분을 분석한다는 말은 문장이 주어졌을 때 주어, 목적어, 서술어 등으로 나누어 생각해본다는 뜻입니다. 문장성분을 확인할 땐 형태론적, 통사론적, 의미론적 기준이 있습니다.

형태론적 기준은 격조사 등이 결합한 양상(형태)를 가지고 따져보는 것입니다. 주격조사 ‘-이/가’가 붙은 명사(구)는 형태론적 기준으로 봤을 때 주어가 될 수 있는 후보가 됩니다. 통사론적 기준은 다른 말과의 문법적 관계를 가지고 따져보는 것입니다. 예컨대 서술어에 선어말어미 ‘-시-‘가 실현됐다고 했을 때 ‘-시-‘와 호응하는 대상이 뭔지 분석해 해당 성분을 주어로 보는 것입니다. 마지막으로 의미론적 기준은 해당 문장성분의 의미적인 내용을 가려보는 것인데요, 해당 성분이 행위주역(Agent)인지 등을 따져보게 됩니다.

그러나 이 세 가지 기준이 항상 절대적으로 작용하는 것은 아닙니다. 가령 형태론적 기준만 해도 완전하지 않습니다. 한국어에서는 대개 격조사 ‘-이/가’가 붙으면 해당 명사(구)는 주어인 경향이 있습니다. 그런데 격조사가 붙은 양상(형태)만 가지고 해당 명사(구)를 주어라고 판단할 경우 문제가 발생할 수 있습니다. 다음 예문을 보겠습니다.

진이가 의사가 되었다.

위 예문에서 전체 문장에서 주어는 ‘진이가’로 보는게 적절합니다. ‘의사가’의 경우 ‘-이/가’가 붙은 모양만 가지고는 주어 같지만 문장성분으로는 주어가 아닙니다(보어). 반대로 ‘-이/가’가 붙지 않았는데 주어인 사례도 존재합니다. 다음 예문과 같습니다. 아래 문장에서 주어는 ‘우리 학교에서’가 분명합니다.

우리 학교에서 대회를 개최했다.

이 글에 정리된 내용은 문장성분을 분석하는 절대적인 기준이라기보다는 한국어 문장성분의 전형적인 특성이라고 이해하시는 게 좋겠습니다.

문장성분의 종류

한국어 문장성분에는 크게 세 가지 종류가 있습니다. 하나의 문장이 성립하기 위해 반드시 필요한 문장성분인 주성분, 반드시 필요한 요소는 아니면서 주로 주성분을 수식하는 기능을 하는 문장성분인 부속성분, 다른 말과 문법적 관계를 맺지 않고 독립되어 있는 성분인 독립성분이 바로 그것입니다. 주성분에는 주어, 서술어, 목적어, 보어 네 가지가 있고, 부속성분에는 관형어, 부사어, 독립성분에는 독립어가 있습니다. 일곱가지 문장성분을 차례로 정리해보겠습니다.

주어

주어란 서술어의 동작 또는 상태나 성질의 주체를 가리킵니다. 우선 형태론적 특징 먼저 보겠습니다. 일반적으로 명사구나 명사절에 주격 조사가 결합하여 실현되나, 보조사가 결합하거나, 조사 없이 실현되기도 합니다. 다음 예문과 같습니다.

구분 예문
명사에 주격조사 결합 눈이 많이 왔다, 할머니께서 떡을 사 오셨다.
명사구/명사절에 주격조사 결합 걷는 사람들이 많다, 우리 학교가 승리하였음이 틀림없다.
보조사 결합 진이는/도/만 울고 있다
조사 없이 실현 어디 사니?

주어의 통사론적 특성은 크게 네 가지로 구분됩니다. 첫째 전형적인 한국어 문장의 주어는 다른 성분에 비해 앞에 나오는 경향이 있습니다. 다음 예문에서 (ㄱ)의 경우 좋아하는 주체가 ‘진이’이며 (ㄴ)은 ‘철수’라는 점을 단박에 알 수 있습니다.

(ㄱ) 진이 철수 좋아해

(ㄴ) 철수 진이 좋아해

둘째 주어 자리에 오는 명사가 존대의 대상이면 서술어인 용언에 선어말어미 ‘-시-‘가 결합합니다. (ㄷ)에서 주어이자 높임의 대상은 ‘선생님’이기 때문에 ‘-시-‘가 쓰였고, (ㄹ)에서 ‘내가’는 주어이지만 높임의 대상이 아니고 ‘선생님을’은 높임의 대상이지만 주어가 아니기 때문에 ‘-시-‘가 쓰이면 비문이 됩니다.

(ㄷ) 선생님께서 나를 보셨어

(ㄹ) 내가 선생님을 *보셨어

셋째 한국어의 주어는 재귀대명사의 선행사가 됩니다. (ㅁ)의 재귀대명사 ‘자기’는 주어인 ‘진이’를 가리킵니다. (ㅂ)의 ‘자기’는 문장 전체의 주어인 ‘진이’를 가리킬 수도 있고, ‘아이들’을 가리킬 수도 있습니다. 그런데 (ㅂ)의 서술어 ‘보내다’는 ‘가게 하다’는 사동(使動)의 의미를 가지는 동사로써 ‘아이들’이 이동의 의미상 주어로 해석할 여지가 있습니다.

(ㅁ) 진이는 졸지에 자기 보호자가 됐다

(ㅂ) 진이$i$는 아이들$j$을 자기$i/j$으로 보냈다

넷째 주어가 복수일 때 다른 성분에도 ‘-들’이 연결되게 할 수 있습니다. 다시 말해 (ㅅ)과 같이 주어의 복수성이 다른 성분에도 옮겨가서 실현된다는 것입니다. 그러나 (ㅇ)과 같이 목적어가 복수인 경우 다른 성분에 ‘-들’이 붙을 수 없습니다.

(ㅅ) 사람들이 많이들 왔다

(ㅇ) 진이야, 밖에 나가서 친구들을 좀 *만나들

예문을 통해 실제 주어 판단을 해보겠습니다.

진이에게 집이 있다.

형태론적으로 따져봤을 때는 ‘집’에 ‘-이/가’가 붙어 주어일 가능성이 있습니다. ‘진이’의 경우 ‘-에게’가 붙어 주어임을 확신하지 못하겠습니다. 통사론적 기준을 적용해 보겠습니다. 우선 다음과 같이 ‘-시-‘와의 호응을 따져봅니다.

(ㅈ) 선생님께는 낡은 집이 한 채 있으시다.

(ㅊ) *진이에게는 존경하는 선생님이 한 분 있으시다.

주어 자리에 높임의 대상을 넣어봄으로써 수행할 수 있습니다. (ㅈ)은 되고 (ㅊ)은 안되는걸로 봐서 ‘진이에게’가 주어일 가능성이 있습니다. 이번에는 재귀대명사를 넣어서 확인해 보겠습니다.

(ㅋ) 진이에게 자기 집이 있다.

(ㅎ) 자기에게 진이 집이 있다.

(ㅋ)의 ‘자기’가 가리키는 대상은 ‘진이’이며 문장이 성립합니다. 하지만 (ㅎ)의 ‘자기’는 ‘진이 집’을 가리키지 않습니다. 따라서 통사론적인 기준으로는 ‘진이에게’가 문장의 주어로 판단됩니다. 이같이 처격조사로 실현되는 주어를 처격주어라고 합니다.

목적어

서술어의 동작의 대상을 목적어라고 합니다. 목적어의 형태론적 특징은 다음과 같습니다. 일반적으로 명사구, 명사절에 격조사 ‘-을/를’이 결합하여 실현되나, 보조사가 결합하거나, 조사 없이 실현되기도 합니다. 예문을 보겠습니다.

구분 명사 명사구(절)
‘-을/를’의 결합 진이는 와인을 마신다. 진이가 새 책을 샀다.
보조사의 결합 진이는 와인은/도/만 마신다. 내가 너희들에게 어떻게 해 주기를 원하느냐?
조사 없이 실현 먹을래? -

목적어의 통사론적인 특성은 해당 문장이 피동문으로 바뀔 때 목적어가 주어가 된다는 점입니다. 예문을 보겠습니다.

(1) 진이는 이 책을 세 번을 읽었다.

(2) 이 책이 진이한테 세 번을 읽혔다.

(3) *세 번이 진이한테 이 책을 읽혔다.

(1)에서 목적어가 ‘이 책을’인지, ‘세 번을’인지 확실치 않습니다. 이를 피동문으로 바꿔 확인합니다. (2)는 ‘이 책을’을 주어로, (3)은 ‘세 번을’을 주어로 바꾼 피동문입니다. (3)이 비문이 되므로 (1)의 목적어는 ‘이 책을’이라고 판단하는 것입니다.

보어

학교문법에서 보어는 동사 ‘되다’와 형용사 ‘아니다’ 앞에 오는, 주어가 아닌 ‘명사구+이/가’ 구성만을 가리킵니다. 다음 예문과 같습니다.

진이가 대학생이 되었다.

침대는 가구가 아닙니다.

하지만 이 구성 말고도 주어와 목적어가 아니면서 서술어가 필수적으로 요구하는 성분, 즉 보어가 되는 사례가 존재합니다. 다음 예문을 보겠습니다.

진이는 서울에 산다.

이 그림은 실물과 똑같다.

어머니는 진이를 수양딸로 삼았다.

위 예문에서 볼드 표시한 어절 없이는 문장이 성립하지 않습니다. 다시 말해 보어의 요건을 충족시킨다는 것입니다. ‘-에’, ‘-로’, ‘-와’는 대개 부사어를 만들어 주는 조사이지만 위 예문처럼 문장의 필수 성분인 ‘보어’를 만들어 주는 경우 또한 존재합니다.

더구나 아래와 같은 예문에서 ‘대학생이’와 ‘가구가’는 문장의 필수 성분, 즉 보어임에도 학교문법의 견해에 따르면 보어가 아니게 됩니다. 따라서 ‘되다’, ‘아니다’ 앞의 ‘명사구+이/가’ 말고도 특정 동사나 형용사가 요구하는 필수적인 성분들 역시 보어에 포함하는 것이 더 적절할 듯 합니다.

진이가 대학생이 맞다, 침대는 가구가 맞다

진이가 대학생이 틀림없다, 침대는 가구가 틀림없다

서술어

서술어란 주어의 동작이나 상태를 서술하는 말입니다. 형태론적으로는 동사, 형용사, ‘명사+이다’와 어미로 이뤄진 구성입니다. ‘이다’나 ‘하다’가 생략된 채로 서술어가 되기도 합니다. 다음 예문을 보겠습니다.

구분 예문
동사+어미 밤하늘에 별이 반짝인다.
형용사+어미 진이는 예쁘다.
명사+이다 저 건물이 서울역이다.
부사(절)+이다 우리가 목적지에 도착한 것은 새벽 두 시가 가까워서입니다.
‘하다’의 생략 한국 등반대 드디어 정상을 정복.
본용언+보조용언 진이가 페인트를 닦아 냈다.

위 예문에서 ‘부사(절)+이다’ 구성을 좀 더 살펴보겠습니다. 부사(절)이 명사처럼 역할을 하면서 ‘이다’와 결합한 ‘부사절+이다’ 구성 전체가 서술어 역할을 하고 있음을 확인할 수 있습니다. ‘A는 B이다’ 형식의 분열문의 일종이라고 보면 좋을 것 같습니다.

마지막으로 ‘본용언+보조용언’ 구성 전체가 서술어 역할을 하는 경우도 있습니다. 본용언과 보조용언 사이에 다른 말이 끼어들 수 없고, 다음 예문처럼 성분 변화시 한 덩어리처럼 기능하기 때문에 ‘본용언+보조용언’ 구성이 하나의 서술어 역할을 한다고 분석합니다.

진이가 페인트를 닦아 낸다.

페인트가 진이에 의해 닦아 내졌다.

이번엔 서술어의 통사론적 특징을 보겠습니다. 우선 서술어는 주어, 목적어, 보어를 거느립니다. 대개 다른 성분보다 뒤에 나타납니다. 마지막으로 부사어의 수식을 받을 수 있습니다. 다음 예문과 같습니다.

밤하늘에 별이 더욱 반짝인다.

진이는 아주 예쁘다.

저 건물이 정말 서울역이다.

관형어

관형어는 명사를 꾸며주는 부속 성분입니다. 보통의 경우에 문장 성립에 있어서 필수적으로 요구되는 성분은 아니지만 의존명사는 관형어를 필수적으로 요구하기도 합니다. 다음 예문과 같습니다.

싼 것이 좋다.

우리는 거기에 가 본 적이 없습니다.

관형사가 관형어로 쓰이기도 하고, ‘명사구+의’, 동사나 형용사에 관형형 어미가 결합된 것이 쓰이기도 합니다. 다음은 관형어의 형태별 예문입니다.

구분 예문
관형사 우리는 책을 후배들에게 물려 주었다.
명사(구)+의 나는 비틀즈의 노래를 좋아한다.
명사 진이는 시골 풍경을 좋아한다.
용언+관형형 어미 아름답던 마을이 폐허가 되었다.
관형사절 너는 진이가 어제 귀국한 사실을 몰랐니?

그러나 관형어는 다른 문장성분들과는 그 층위가 대등하지 않습니다. 즉 관형어는 주어나 목적어, 보어가 아니라 ‘명사’를 수식합니다. 예문을 보겠습니다.

성실한 학생이 왔다.

위 문장에서 주어는 ‘성실한 학생’이고, 관형어 ‘성실한’은 ‘학생’을 수식합니다. 다시 말해 주어 내부에 관형어가 있는 구성입니다.

부사어

부사어는 주로 서술어를 꾸며주는 부속 성분입니다. 부사가 부사어로 쓰이기도 하고, ‘명사+부사격조사’, 동사나 형용사에 부사형 어미가 결합된 것이 부사어로 쓰이기도 합니다. 예문을 보겠습니다.

구분 예문
부사 우리는 자주 만났다.
명사+부사격조사 우리는 서울에서 만났다.
용언+부사형 어미 우리는 늦게 만났다.
용언+부사성 의존명사 놀 만큼 놀았다, 어제 했던 대로 해 보아라.
부사절 사람들이 떠드는 소리에 잠을 자지 못했다.

독립어

독립어는 문장의 어느 성분과도 직접적인 관련 없이 쓰입니다. 독립어를 감동어, 호격어, 접속어, 제시어 네 가지로 나누기도 합니다.

감동어는 이어지는 문장이 없이도 사용에 장애를 받지 않으며, 놓이는 순서가 매우 자유롭습니다. 감탄사가 여기에 속합니다. 예문을 보겠습니다.

아! 저기는 단풍의 바다로구나.

, 저도 가겠습니다.

호격어는 누군가를 부르는 말입니다. 해라체에서는 호격조사 ‘아, 야’로, 하게체부터는 명사구만으로 표시됩니다. 호격조사 ‘이여, 이시여’가 붙은 명사구도 호격어가 될 수 잇습니다. 문장의 가운데나 끝에 놓일 수 있습니다.

진이야, 빨리 학교에 가거라.

이 군, 이리 와서 일 좀 도와 주게.

정 박사, 식사하러 나갑시다.

선생님, 어디가 편찮으십니까?

겨레여, 잠에서 깨어나라 / 임이시여, 나를 떠나지 마시옵소서

접속어는 접속부사 가운데서 단어 및 어절 접속의 ‘또는, 혹은, 및’을 제외한 나머지 접속부사를 가리킵니다.

정직하게 살아라. 그리고 열심히 노력해라.

어느 나라 사람이나 먹는 것은 다 같다. 그러나 먹는 방법과 양식이 다르다.

제시어는 뒤의 내용을 대표하는 명사구를 첫머리에 제시함으로써 상대방으로 하여금 주의를 집중토록 하는 성분입니다. 명사구로만 성립하고 특별한 조사가 붙는 일이 없습니다.

청춘, 이는 듣기만 하여도 가슴이 설레는 말이다.

Comment  Read more

List, Linked List

|

이번 글에서는 리스트(List)연결리스트(Linked List) 개념에 대해 살펴보도록 하겠습니다. 이 글은 고려대 김황남 교수님 강의를 정리하였음을 먼저 밝힙니다. 파이썬 코드는 이곳을 기본으로 하되 조금 수정하였습니다. 그럼 시작하겠습니다.

리스트

리스트란 같은 값이 한번 이상 존재할 수 있고 순서가 있는 일련의 값이 모여있는 추상적 자료형(Abstract Data Type)입니다. 예컨대 리스트 (C, Y, R)은 (Y, C, R)과 다릅니다. 리스트에는 Add, Delete, Access(read/change) 연산이 있습니다.

Insert

주어진 리스트의 첫번째 위치에 한 요소를 추가하는 연산의 계산복잡성은 리스트가 $n$개 요소로 구성돼 있을 때 $O(n)$이 됩니다. $n$개 요소 각각의 위치를 오른쪽으로 한 칸씩 모두 옮겨야 하기 때문입니다. $k$번째 위치에 새로운 요소를 추가할 경우 계산복잡성은 $O(n-k)=O(n)$입니다. 한편 주어진 리스트 마지막 위치에 요소를 추가하는 연산은 $O(1)$입니다. 파이썬은 내장함수로 리스트 추가 연산을 제공하는데요. 다음과 같습니다.

a = [1, 2, 3]
a.insert(0, 4)
# [4, 1, 2, 3]

Delete

주어진 리스트의 첫번째 위치에 한 요소를 삭제하는 연산의 계산복잡성은 리스트가 $n$개 요소로 구성돼 있을 때 $O(n)$이 됩니다. $n-1$개 요소 각각의 위치를 왼쪽으로 한 칸씩 모두 옮겨야 하기 때문입니다. $k$번째 위치에 있는 요소를 삭제할 경우 계산복잡성은 $O(n-k)=O(n)$입니다. 한편 주어진 리스트 마지막 위치의 요소를 삭제하는 연산은 $O(1)$입니다. 파이썬은 내장함수로 리스트 삭제 연산을 제공하는데요. 다음과 같습니다.

a = [1, 2, 3]
del a[0]
#[2, 3]

Access

주어진 리스트의 특정 위치에 있는 요소값을 읽거나 바꾸는 Access 연산의 계산복잡성은 $O(1)$입니다. 파이썬은 내장함수로 리스트 Access 연산을 제공하는데요. 다음과 같습니다.

a = [1, 2, 3]
a[0] # 1
a[1] = 4 # [1, 4, 3]

Scalability issue

리스트라는 자료형의 단점은 요소의 최대 개수를 사전에 정해야 하고, 그 개수를 넘어서는 요소들을 입력할 수 없다는 점입니다. 이를 scalability issue라고 합니다. 실제로 파이썬에서 다음과 같은 명령어를 입력했을 때 에러가 납니다. 앞으로 설명할 연결리스트는 이 단점을 극복한 자료구조입니다.

a = [1, 2, 3]
a[3] = 1 # IndexError: list assignment index out of range

연결리스트

연결리스트란 각 노드가 연결되어 있는 방식으로 데이터가 저장돼 있는 추상적 자료형입니다. 한 노드는 해당 노드의 실제값(value)과 다음 노드의 주소값이 담긴 포인터(pointer)로 구성돼 있습니다. 마지막 노드의 포인터는 Null값을 갖습니다. 다음과 같습니다.

연결리스트는 포인터의 존재 덕분에 리스트의 길이를 사전에 정할 필요가 없습니다. 포인터만 조금 바꿔주면 리스트 요소의 추가가 얼마든지 가능하기 때문입니다.

위의 각 노드를 파이썬으로 구현한 코드는 다음과 같습니다. 각 노드는 개별 클래스로 구성되며 각 노드(클래스)는 실제값(변수명 data에 저장)과 포인터(변수명 next_node에 저장)로 구성됩니다.

class Node(object):
    def __init__(self, data=None, next_node=None):
        self.data = data
        self.next_node = next_node
    def set_next(self, new_next):
        self.next_node = new_next

Insert

주어진 연결리스트의 첫번째 위치에 한 요소를 추가하는 연산의 계산복잡성은 리스트가 $n$개 요소로 구성돼 있을 때 $O(1)$이 됩니다. 전체 요소의 인덱스를 변경할 필요 없이 추가할 노드가 가리키는 다음 위치(포인터)를 기존 연결리스트의 첫번째 요소(head)에 연결하고, 추가할 새로운 노드를 기존 연결리스트의 첫번째 요소로 정의하기만 하면 되기 때문입니다. 다음과 같습니다.

class LinkedList(object):

    def __init__(self, head=None):
        self.head = head

    def insert(self, data):
        new_node = Node(data)
        # 추가 노드의 다음 위치를 기존 head에 연결
        new_node.set_next(self.head)
        # 추가 노드를 새로운 head로 정의
        self.head = new_node

Delete

주어진 연결리스트의 첫번째 위치의 요소(head)를 삭제하는 연산의 계산복잡성은 리스트가 $n$개 요소로 구성돼 있을 때 $O(1)$이 됩니다. 전체 요소의 인덱스를 변경할 필요 없이 기존 head의 다음 다음번 노드의 위치를 저장해 두었다가(new_head_next_node), 기존 head의 다음 노드의 값을 새로운 head의 값으로 하고 new_head_next_node를 새로운 head의 포인터로 하는 새로운 head를 재정의해주기만 하면 되기 때문입니다. 다음과 같습니다.

    def delete(self):
        new_head_next_node = self.head.next_node.next_node
        self.head = Node(self.head.next_node.data)
        self.head.next_node = new_head_next_node

Access

주어진 연결리스트의 특정 위치에 있는 요소값을 읽거나 바꾸는 Access 연산의 계산복잡성은 $O(n)$입니다. 예컨대 $k$번째 위치에 있는 값을 읽으려면 $k$개의 요소를 읽어들여야 하기 때문입니다. idx번째 요소에 Access하는 파이썬 코드는 다음과 같습니다.

    def access(self, idx):
        current = self.head
        i = 0
        while i < idx:
            i += 1
            current = current.next_node
        return current.data

실행

구체적인 실행 결과는 다음과 같습니다.

a = LinkedList()
a.insert(1) 
a.insert(2) 
a.insert(3)
a.access(0) # 3
a.delete()
a.access(0) # 2

여러 가지 연결리스트

지금까지 설명해드린 연결리스트는 엄밀히 말해 단일연결리스트(singly linked list)입니다. 그런데 다음과 같은 변형이 존재합니다. 각각 이중연결리스트(doubly linked list), 원형연결리스트(circular linked list)입니다.

이중연결리스트는 Head와 Tail 모두 움직일 수 있어 Access 연산시 계산복잡성을 줄일 수 있습니다. 원형연결리스트는 링 버퍼(ring buffer) 등과 같이 메모리 효율성이 중요한 디바이스에서 널리 사용되고 있다고 합니다.

Comment  Read more

포인터(pointer)

|

이번 글에서는 포인터(pointer) 개념에 대해 살펴보도록 하겠습니다. 이 글은 고려대 김황남 교수님 강의를 정리하였음을 먼저 밝힙니다. 그럼 시작하겠습니다.

개념

포인터란 다른 변수의 메모리 공간주소를 가리키는 변수를 가리킵니다. 포인터는 C, C++ 등과 같은 언어에서는 프로그래머가 직접 제어할 수 있고, 파이썬 등과 같은 언어에서는 완전히 숨겨져 사용할 수 없습니다. 하지만 프로그래머가 사용할 수 없다고 해서 해당 언어에서 포인터가 전혀 쓰이지 않는 건 아니어서 그 개념을 알고 있을 필요가 있습니다.

예시

포인터를 명시적으로 다룰 수 있는 C언어에서 다음과 같은 코드가 있다고 칩시다.

int i, *pi, **ppi;
i = 5;
pi = &i;
ppi = &pi;

위 코드를 그림으로 나타내면 다음과 같습니다.

$i$, $pi$, $ppi$가 가리키는 메모리 주소와 그 실제값은 다음과 같습니다.

Variable Address Value
$i$ 100 5
$pi$ 104 100
$ppi$ 108 104

각 notation의 의미와 그 notation이 가리키는 값은 다음과 같습니다.

Usage Meaning Value
$pi$ $i$의 주소 100
$*pi$ $i$의 실제값 5
$\&pi$ $pi$의 주소 104
$ppi$ $pi$의 주소 104
$*ppi$ $pi$의 실제값=$i$의 주소 100
$**ppi$ $i$의 실제값 5

Comment  Read more

퀵 정렬(Quick Sort)

|

이번 글에서는 퀵 정렬(Quick Sort) 알고리즘에 대해 살펴보도록 하겠습니다. 이 글은 고려대 김선욱 교수님 강의와 위키피디아를 정리하였음을 먼저 밝힙니다. 파이썬 코드 구현은 이곳을 참고하였습니다. 그럼 시작하겠습니다.

개념

퀵 정렬은 분할정복(divide and conquer) 방식으로 작동합니다. 그 절차는 다음과 같습니다.

  • 리스트 가운데서 하나의 원소를 고릅니다. 이를 피벗(pivot)이라 합니다.
  • 피벗 앞에는 피벗보다 작은 값, 뒤에는 큰 값이 오도록 하여 리스트를 둘로 분할합니다.
  • 분할된 두 개 리스트 각각에 재귀적으로 이 과정을 반복합니다.

예시

다음과 같은 리스트를 정렬해보겠습니다.

[5, 3, 7, 6, 2, 1, 4]

첫번째 값(5)을 피벗으로 택해보겠습니다. (마지막 요소 4를 택해도 관계 없습니다) 이 값보다 작은 값들로만 구성된 리스트와 큰 값들로만 구성된 리스트 둘로 분할합니다. 이를 각각 LESSORGREATER라고 명명해보겠습니다.

LESSOR = [3, 2, 1, 4]

GREATER = [7, 6]

그리고 나서 LESSOR와 GREATER 각각에 같은 작업을 해당 리스트의 요소 개수가 하나가 될 때까지 재귀적으로 반복합니다.

구현

퀵 정렬을 파이썬으로 구현한 코드는 다음과 같습니다.

def quick_sort(ARRAY):
    ARRAY_LENGTH = len(ARRAY)
    if( ARRAY_LENGTH <= 1):
        return ARRAY
    else:
        PIVOT = ARRAY[0]
        GREATER = [ element for element in ARRAY[1:] if element > PIVOT ]
        LESSER = [ element for element in ARRAY[1:] if element <= PIVOT ]
        return quick_sort(LESSER) + [PIVOT] + quick_sort(GREATER)

계산복잡성

퀵 정렬의 계산복잡성은 피벗을 어떻게 선택하느냐에 따라 달라집니다. 최악의 경우는 다음과 같습니다. 다시 말해 피벗의 왼쪽(LESSOR) 요소가 매번 하나인 경우입니다. 이렇게 되면 높이가 $n$, 각 층에서 $n$개의 요소에 대해 정렬을 수행해야 하므로 $O(n^2)$의 계산복잡도를 가지게 됩니다.

가장 좋은 경우는 다음과 같습니다. 분할 과정이 다음과 같이 균형적이어서, 계산 트리의 높이가 $n$에서 $\log_2{n}$으로 줄어들게 되기 때문입니다. 이렇게 되면 높이가 $\log_2{n}$, 각 층에서 $n$개의 요소에 대해 정렬을 수행해야 하므로 $O(n\log_2{n})$의 계산복잡도를 가지게 됩니다.

Average case의 경우에도 퀵 정렬은 $O(n\log{n})$의 계산복잡도를 가진다고 합니다. 아울러 피벗을 선택할 때 정해진 위치가 아니라 랜덤하게 선택하거나, 몇 개 값을 랜덤 샘플링해 이 값들의 중앙값(median)에 가까운 값을 피벗으로 정하면 계산복잡도를 다소 낮추는 데 도움이 된다고 합니다.

퀵 정렬의 특징

설명의 편의를 위해 피봇을 기준으로 작은 값을 왼쪽, 나머지를 오른쪽으로 보내는 과정을 재귀적으로 반복한다고 했습니다만, original code는 이보다는 살짝 더 복잡합니다. 정렬 수행 과정에서 별도 저장 공간을 필요로 하지 않는 in-place sort를 지향하고자 하기 때문인데요. original code의 수행 과정을 보시려면 이곳을 참고하시면 좋을 것 같습니다. 수행과정을 도식화하면 다음 그림과 같습니다. 이 과정에서 같은 값의 상대적 위치가 바뀔 수 있습니다(unstable sort).

Comment  Read more