[Data Science from Scratch] Ch4. Linear Algebra

    ๋ฐ˜์‘ํ˜•

    Linear algebra is the branch of mathematics that deals with vector spaces.

    ๐Ÿ“Œ Vectors

    ์ถ”์ƒ์ ์œผ๋กœ, vector๋Š” ์„œ๋กœ ๋”ํ•˜๊ณ , scalar๋ฅผ ๊ณฑํ•˜๊ณ , ์ƒˆ๋กœ์šด ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด์ด๋‹ค.
    ๊ตฌ์ฒด์ ์œผ๋กœ๋Š”, vector๋Š” ์–ด๋–ค ์œ ํ•œํ•œ ์ฐจ์›์˜ ๊ณต๊ฐ„์—์„œ์˜ ์ ์ด๋‹ค. ์ง€๊ธˆ๊นŒ์ง€ ๋ณด์•˜๋˜ data๋ฅผ vector๋ผ๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š๊ฒ ์ง€๋งŒ, ๊ทธ data๋“ค์€ numeric data๋ฅผ ๋Œ€ํ‘œํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ์˜ˆ๊ฐ€ ๋œ๋‹ค.

    ์˜ˆ๋ฅผ ๋“ค์–ด, ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์˜ ํ‚ค, ๋ชธ๋ฌด๊ฒŒ, ๋‚˜์ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์„ ๋•Œ, ๊ทธ ๋ฐ์ดํ„ฐ๋Š” 3์ฐจ์›์˜ vector๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋‹ค. ํ˜น์€ ํ•™์ƒ๋“ค์˜ ์‹œํ—˜ ์„ฑ์  ๋ฐ์ดํ„ฐ๋ฅผ 4์ฐจ์›์˜ vector(exam1, exam2, exam3, exam4)๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

    vector๋ฅผ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ ์ˆซ์ž๋“ค์˜ list๋กœ ํ‘œํ˜„ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. 3๊ฐœ์˜ ์ˆซ์ž๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋Š” list์™€ 3์ฐจ์›์˜ ๋ฒกํ„ฐ๋Š” ์„œ๋กœ ๋Œ€์‘๋  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

    Abstractly, vectors are objects that can be added together(to form new vectors) and that can be multiplied by scalars, also to form new vectors.
    
    Concretely (for us), vectors are points in some finite-dimensional space. Although you might not think of your data as vectors, they are a good way to represent numeric data.
    
    For example, if you have the heights, weights, and ages of a large number of peaple, you can treat your data as three-dimensional vectors(`height, weight, age`). If you're teaching a class with four exams, you can treat student grades as four-dimensional vectors(`exam1, exam2, exam3, exam4`)
    
    The simplest from-scratch approach is to represent vectors as list of numbers. A list of three numbers corresponds to a vector in three-dimensional space, and vice versa:
    height_weight_age = [70, 170, 40] # height(inches), weight(pounds), age(years)
    grades = [95, 80, 75, 62] # exam1, exam2, exam3, exam4

    ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ์ ‘๊ทผํ–ˆ์„ ๋•Œ์˜ ๋ฌธ์ œ์ ์€ ํŒŒ์ด์ฌ์—์„œ list๋Š” vector๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๋Š” vector์— ๋Œ€ํ•œ ์ˆ˜ํ•™์ ์ธ ๋„๊ตฌ๋“ค์„ ์šฐ๋ฆฌ ์Šค์Šค๋กœ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
    ์˜ˆ๋ฅผ ๋“ค๋ฉด, ๋‘ ๊ฐœ์˜ ๋ฒกํ„ฐ๋ฅผ ๋”ํ•˜๋Š” ๊ฒƒ์„ ์ƒ๊ฐํ•ด๋ณด์ž.

    Vectors add componentwise : ๋‘ ๊ฐœ์˜ ๋ฒกํ„ฐ์˜ ๊ธธ์ด๊ฐ€ ๊ฐ™๋‹ค๋ฉด, ๋‘ ๋ฒกํ„ฐ์˜ ํ•ฉ์€ ์„œ๋กœ ๊ฐ™์€ ์œ„์น˜์˜ ์ธ๋ฑ์Šค์— ์žˆ๋Š” ๊ฐ’๋“ค๋ผ๋ฆฌ ๋”ํ•œ list์ด๋‹ค. (If they're not the same length, then we're not allowed to add them.)
    ex) [1, 2] + [2, 1] result in [1+2, 2+1] or [3, 3]

    def vector_add(v, w):
        """adds corresponding elements"""
        return [v_i + w_i
                for v_i, w_i in zip(v, w)]
    def vector_subtract(v, w):
        """subtracts corresponding elemnts"""
        return [v_i - w_i
               for v_i, w_i in zip(v, w)]

     

    We'll also sometimes want to componentwise sum a list of vectors. That is, create a new vector whose first element is the sum of all the first elemnts, whose second element is the sum of all of second elemnts, and so on. The easiest way to do this is by adding one vector at a time:

    def vector_sum(vectors):
        """sums all corresponding elements"""
        result = vectors[0] # start with first vector
        for vector in vectors[1:]: # then loop over the others
            result = vector_add(result, vector) # and add them to the result
        return result

     

    we can rewrite this more briefly

    from functools import partial, reduce
    
    def vector_sum(vectors):
        return reduce(vector_add, vectors)
    
    vector_sum = partial(reduce, vector_add) # this one is more clever and helpful
    vector_sum([[1, 2, 3], [1, 2, 3], [1, 2, 3]])

    [3, 6, 9]

     

    def scalar_multiply(c, v):
        """c is a number(scalar), v is a vector"""
        return [c * v_i for v_i in v]
    def vector_mean(vectors):
        """compute the vector whose ith element is the mean of the
        ith elements of the input vectors"""
        n = len(vectors)
        return scalar_multiply(1/n, vector_sum(vectors))

     

    The dot product of two vectors is the sum of their componentwise products:

    def dot(v, w):
        """v_i * w_i + ... + v_n * w_n"""
        return sum(v_i * w_i
                  for v_i, w_i in zip(v, w))
    dot([1, 2], [1, 2])

    5

     

    dot product๋Š” v๋ฒกํ„ฐ๊ฐ€ w์˜ ๋ฐฉํ–ฅ์œผ๋กœ ์–ผ๋งˆ๋‚˜ ํ™•์žฅ๋  ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ ์ธก์ •ํ•œ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด, w = [1, 0]์ด๋ฉด dot(v, w)๋Š” v๋ฒกํ„ฐ์™€ ๊ฐ™๋‹ค.
    ์ด๊ฒƒ์€ v๋ฅผ w์— _project_ํ–ˆ์„ ๋•Œ ๊ทธ ๋ฒกํ„ฐ์˜ ๊ธธ์ด์™€ ๊ฐ™๋‹ค.
    The dot product measures how far the vector v extends in the w direction. For example, if w=[1, 0] then dot(v, w) is just the first component of v. Another way of saying this is that it's the length of the vector you'd get if you projucted v onto w

    doc product as vector projection

    ์ด๋ฅผ ์ด์šฉํ•ด์„œ vector์˜ ์ œ๊ณฑํ•ฉ_sum of square_๋ฅผ ์‰ฝ๊ฒŒ ๊ตฌํ•  ์ˆ˜ ์žˆ๋‹ค.

    def sum_of_squares(v):
        """v_1 * v_1 + v_2 * v_2 + ... + v_n * v_n"""
        return dot(v, v)
    sum_of_squares([3, 4])

    25

     

    import math
    def magnitude(v): #magnitude of vector means length of vector
        return math.sqrt(sum_of_squares(v)) #math.sqrt is square root function
    magnitude([3, 4])

    5.0

     

    let's get a distance of two vectors!

    def squared_distance(v, w):
        """(v_1 - w_1) ** 2 + ... (v_n - w_n) ** 2"""
        return sum_of_squared(vector_subtract(v, w))
    
    def distance(v, w):
        return math.sqrt(sqquared_disatnce(v, w))
    def distance(v, w):
        return magnigude(vector_subtract(v, w))

     

     

    ๐Ÿ“Œ Matrix

    matrix๋Š” 2์ฐจ์›์œผ๋กœ ๊ตฌ์„ฑ๋œ ์ˆซ์ž๋“ค์˜ ๋ชจ์Œ์ด๋‹ค. ํŒŒ์ด์ฌ์—๋Š” ๋งคํŠธ๋ฆญ์Šค๋ฅผ ์ค‘์ฒฉ๋œ listํ˜•ํƒœ๋กœ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด A๊ฐ€ ํ–‰๋ ฌ์ด๋ผ๋ฉด, A[i][j]๋Š” i๋ฒˆ์งธ row์˜ j๋ฒˆ์งธ column์˜ ๊ฐ’์„ ๋งํ•œ๋‹ค. (๋ณดํ†ต, ํ–‰๋ ฌ์˜ ์ด๋ฆ„์€ ๋Œ€๋ฌธ์ž๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.)

    A = [[1, 2, 3],
        [4, 5, 6]] # A has 2 rows and 3 columns
    B = [[1, 2],
        [3, 4],
        [5, 6]] # B has 3 rows and 2 columns
    def shape(M):
        num_rows = len(M)
        num_cols = len(M[0]) if M else 0 # number of elements in first row
        return num_rows, num_cols
    shape(A)

    (2, 3)

     

    matrix๊ฐ€ n๊ฐœ์˜ row์™€ k๊ฐœ์˜ column์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด, ์ด ํ–‰๋ ฌ์„ nโœ•k matrix๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.
    ์—ฌ๊ธฐ์„œ nโœ•k matrix์˜ ๊ฐ๊ฐ์˜ row๋“ค์„ ๊ธธ์ด๊ฐ€ k์ธ ๊ฐ๊ฐ์˜ ๋ฒกํ„ฐ๋“ค์ด๊ณ  / ๊ฐ๊ฐ์˜ column๋“ค์„ ๊ธธ์ด๊ฐ€ n์ธ ๊ฐ๊ฐ์˜ ๋ฒกํ„ฐ๋“ค๋กœ ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค.

    def get_row(M, i):
        return M[i]
    
    def get_column(M, j):
        return [M_i[j]
               for M_i in M]
    get_row(A, 0)

    [1, 2, 3]

     

    get_column(A, 1)

    [2, 5]

     

    def make_matrix(num_rows, num_cols, entry_fn):
        """returns a num rows x num cols matrix
        whose (i,j)th entry is entry_fn(i, j)"""
        return [[entry_fn(i, j) # given i, create a list
                for j in range(num_cols)] # [entry_fn(i, 0), ...]
                for i in range(num_rows)] # create one list for each i
    def is_diagonal(i, j):
        """1's on the 'diagonal', 0's everywhere else"""
        return 1 if i == j else 0
    identity_matrix = make_matrix(5, 5, is_diagonal)
    identity_matrix

    [[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]]

     

     

    matrix๊ฐ€ ์šฐ๋ฆฌ์—๊ฒŒ ์ค‘์š”ํ•œ ์ด์œ !

    1. matrix๋ฅผ ํ•˜๋‚˜์˜ row๋กœ์„œ ์ทจ๊ธ‰๋˜๋Š” ๊ฐ๊ฐ€์˜ ๋ฒกํ„ฐ๋“ค๋กœ ์ด๋ฃจ์–ด์ง„ ๋ฐ์ดํ„ฐ์…‹์„ ํ‘œํ˜„ํ•  ๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    2. nโœ•k matrix๋ฅผ k์ฐจ์› ๋ฒกํ„ฐ์™€ n์ฐจ์› ๋ฒกํ„ฐ๋ฅผ ๋งคํ•‘ํ•˜๋Š” ์„ ํ˜• ํ•จ์ˆ˜๋ฅผ ํ‘œํ˜„ํ•˜๋Š”๋ฐ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    3. matrix๋“ค์„ binary relationships๋ฅผ ํ‘œํ˜„ํ•˜๋Š”๋ฐ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
      ์˜ˆ๋ฅผ ๋“ค์–ด, ์•„๋ž˜์—์„œ ์›๋ž˜์—๋Š” friendships๋ฅผ (i, j)์Œ๋“ค๋กœ binary ๊ด€๊ณ„๋ฅผ ํ‘œํ˜„ํ–ˆ๋‹ค๋ฉด, ํ–‰๋ ฌ์„ ํ†ตํ•ด์„œ๋Š” ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ๋•Œ 1, ์—†์„ ๋•Œ 0์˜ ๊ฐ’์„ ํ‘œ์‹œํ•จ์œผ๋กœ์จ user ํ•œ ๋ช…๋‹น ํ•˜๋‚˜์˜ vector๋ฅผ ๋Œ€์‘์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
      friendships = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (3, 4), 
      			(4, 5), (5, 6), (5, 7), (6, 8), (7, 8), (8, 9)]
      
      # user0 1 2 3 4 5 6 7 8 9
      friendships = [[0, 1, 1, 0, 0, 0, 0, 0, 0, 0], # user 0
                  [1, 0, 1, 1, 0, 0, 0, 0, 0, 0], # user 1 
                  [1, 1, 0, 1, 0, 0, 0, 0, 0, 0], # user 2 
                  [0, 1, 1, 0, 1, 0, 0, 0, 0, 0], # user 3 
                  [0, 0, 0, 1, 0, 1, 0, 0, 0, 0], # user 4 
                  [0, 0, 0, 0, 1, 0, 1, 1, 0, 0], # user 5 
                  [0, 0, 0, 0, 0, 1, 0, 0, 1, 0], # user 6 
                  [0, 0, 0, 0, 0, 1, 0, 0, 1, 0], # user 7 
                  [0, 0, 0, 0, 0, 0, 1, 1, 0, 1], # user 8 
                  [0, 0, 0, 0, 0, 0, 0, 0, 1, 0]] # user 9

    ์—ฐ๊ฒฐ๋œ ๊ด€๊ณ„๊ฐ€ ์ ๋‹ค๋ฉด, tuple๋กœ ์ €์žฅํ•œ frinedships๋ณด๋‹ค 0์„ ๋” ๋งŽ์ด ์ €์žฅํ•ด์•ผํ•˜๋ฏ€๋กœ ๋น„ํšจ์œจ์ ์ธ friendships๊ฐ€ ๋  ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ, ํ–‰๋ ฌ ํ‘œํ˜„์€ ๋‘ ๊ฐœ์˜ node๊ฐ€ ์—ฐ๊ฒฐ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š”๋ฐ์— ์žˆ์–ด์„œ ํ›จ์”ฌ ๋น ๋ฅด๋‹ค. (๋ชจ๋“  edge๋ฅผ ํƒ์ƒ‰ํ•˜์ง€ ์•Š๊ณ  ํ•˜๋‚˜์˜ matrix๋งŒ ๋ณด๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.)

    print(friendships[0][2] == 1) # True, user0 and user2 are friends.
    print(friendships[0][8] == 1) # False, user0 and user8 are not friends.

    True
    False

     

    ์œ ์‚ฌํ•˜๊ฒŒ, ํ•˜๋‚˜์˜ ๋…ธ๋“œ๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” connection์„ ์ฐพ์„ ๋•Œ๋„ ํ•˜๋‚˜์˜ column(๋˜๋Š” row)๋งŒ ํ™•์ธํ•˜๋ฉด ๋œ๋‹ค.

    friends_of_user5 = [i
                       for i, is_friend in enumerate(friendships[5])
                       if is_friend]
    friends_of_user5

    [4, 6, 7]

    ๋ฐ˜์‘ํ˜•

    ๋Œ“๊ธ€